IBM Skip to main content
Search for:   within 
      Search help  
     IBM home  |  Products & services  |  Support & downloads   |  My account

developerWorks > Open source projects | Java technology
developerWorks
Extend Eclipse's Java Development Tools
code709 KBe-mail it!
Contents:
Your own easy refactoring of member visibility
A brief tour of "Hello World"
Asking the right questions
How do we extend the user interface in general?
How does an extension to the user interface know about basic events?
How do we extend the user interface of specific elements of the JDT?
What is the relationship between elements shown in the Package Explorer and the same elements shown in other views?
How do you change the JDT model programmatically?
How do you analyze Java code to apply modifications?
Where do you go from here?
More about the solution
Resources
About the author
Rate this article
Related content:
Using the Eclipse GUI outside the Eclipse Workbench
Developing Eclipse plug-ins
Developing JFace wizards
C/C++ development with the Eclipse Platform
XML development with the Eclipse Platform
Subscriptions:
dW newsletters
dW Subscription
(CDs and downloads)
What is possible, where to start, and how to proceed

Level: Advanced

Dan Kehn
Senior Programmer, IBM
22 July 2003

The refactoring capability of Eclipse's Java development environment is one of the most useful features it provides. This article will introduce you to the steps for creating your own refactoring as a natural extension of Eclipse. Portions of the solution presented in this article were excerpted from the recently published book, The Java Developer's Guide to Eclipse.

Eclipse has received much fanfare and accolades because of its powerful Java development environment. That -- coupled with the team environment and other base capabilities -- makes Eclipse a compelling integrated development environment, which is great news for Java developers. Moreover, Eclipse is an open source project. But what makes Eclipse truly exciting is the possibilities of extension that it offers you.

A number of commercially available products, based on Eclipse, show the practical implications of this way of delivering integrated products. IBM WebSphere Application Developer and Rational XDE, for example, demonstrate the impact that Eclipse has already had. These products and others based on Eclipse diminish the user's learning curve because of their similar user interface. Sure, this is valuable to large software houses, but what's in it for the "little guy"?

That's where the extensibility story of Eclipse gets interesting. Not just integration for those who have large development organizations, but also for anyone willing to invest some time in learning a few Eclipse frameworks. "Oh no," you may be thinking, "not more frameworks; I don't have the time to learn more frameworks." Don't worry; it will be quick and fairly easy. And before that little voice in your head has time to say it, no, this article will not be a trivial "hello world" extension of Eclipse. Rest assured, you'll see practical value and a clear demonstration of how you can enhance your productive use of Eclipse's Java development environment. You may even be a little surprised to see that it takes only a few dozen lines of code to do some fairly amazing things.

This article will show you what is possible and where to start, and give you a firm appreciation for what's involved in getting there. Though extending Eclipse is an advanced topic, you can start with only passing knowledge of how to use Eclipse's Java development environment (and be sure to check out the suggested reading in Resources for further study).

Your own easy refactoring of member visibility
Initially when writing code, I don't worry too much about categorizing method visibility as default (package), private, public, or protected. As I create methods, I make them all public. Only once I've finalized the organization of packages and finished refactoring methods -- whether that be by extracting new methods from existing code, pulling up or pushing down methods in the hierarchy, or moving them to another class entirely -- do I then review method visibility. I figure that until I know the final class shapes and have a little practical usage of the code, I don't want to declare what my "clients" might need. In other words, before sharing a new framework, one must decide what is implementation detail and what is necessary so others can extend it.

It would be handy if you could merely select methods in the Outline view, Hierarchy view, or wherever you see methods -- and with a click of a menu choice, set one or more methods to the desired visibility. Admittedly, I am accustomed to this capability from my VisualAge for Smalltalk days. Figure 1 shows an extension to Eclipse's Java development environment in the context of the Java editor's Outline view.

Figure 1. Extension of a method's context menu
Extension of method's context menu

This is subtle, from a user's perspective, because of the natural way this was introduced into the user interface. There is no inkling that these new menu choices weren't part of Eclipse's original Java Development Tools (JDT). In fact, that's why the menu cascade is prefixed with "soln" -- so you can tell it's an extension! What's more, the developer doesn't have to remember that these choices are only available in a particular view or editor, because they will be shown anywhere a method is shown.

A brief tour of "Hello World"
"Hey, wait a minute, you promised no 'Hello, World'!" True, but we do need to cover a little about Eclipse's underpinnings before getting to the really interesting things. So if you have never written your own extension to Eclipse, please join me in a quick tour of the Eclipse architecture and plug-in development environment. Otherwise, skip to the next section. On with the tour!

In essence, Eclipse is a collection of loosely bound yet interconnected pieces of code. How these pieces of code are "discovered" and how they discover and extend each other captures the fundamental principles of the Eclipse architecture.

Figure 2. Eclipse Platform architecture
Eclipse Platform architecture

Extension versus Extension Point
Be aware that the XML tags for these two are quite similar. An extension point declares the availability of a plug-in's functionality to other plug-ins and is denoted by the <extension-point> tag. An extension uses a previously defined extension point and is denoted by the <extension> tag having the point attribute naming the extension point it wishes to use.

These functional units are called plug-ins. The Eclipse Platform Runtime, shown in Figure 2, is responsible for finding the declarations of these plug-ins, called a plug-in manifest, in a file named plugin.xml, each located in its own subdirectory below a common directory of Eclipse's installation directory named plugins (specifically <inst_dir>\eclipse\plugins). From these files, it builds at startup a global in-memory registry, called the plug-in registry, from which a given plug-in can determine at runtime what other plug-ins wish to extend it. A plug-in that wishes to allow others to extend it will declare an extension point. This is a sort of "power strip" for a plug-in that others can take advantage of by declaring an extension to it.

Returning to our example, the mission then is to decide where to "plug into" Eclipse by finding the appropriate extension point for our needs. Fortunately, once you have used Eclipse for a while, you know a surprising amount about what is available, perhaps without realizing it. This is because what you see in the Eclipse user interface and what is modeled by the classes that make up the Eclipse plug-ins often correspond nearly one-for-one to each other. Figure 3 makes this point clearer:

Figure 3. Views and their models
Views and their models

Here we see an ordinary progression of user interfaces starting from the lowest common denominator on the right, the file system contents shown by a dir command in a Command Prompt window, continuing to a highly specialized view, that of the JDT's Package Explorer on the left. From a user interface perspective, all these views are visualizing a representation of the same "model," namely some files. As Eclipse users, we naturally expect the two Eclipse views to present us different ways of looking at the same thing simultaneously: the Navigator shows a specialized view of a portion of the operating system files (Eclipse's workspace), while the Package Explorer shows us some of the same files organized and presented in a way that is more natural and efficient for a Java programmer.

Seeing how the Eclipse user interface reflects its underlying model and how its models build upon each other gives us an important clue about how we can find the best place to "plug in" our extension. The Eclipse interface names shown below the views, IFile and ICompilationUnit, are just two examples of the interfaces we can expect from the model that makes up Eclipse. Since they so often correspond with what is shown in the user interface, you already have an intuitive appreciation for what's available programmatically.

That is Part I of our tour. Part II is a look at our developing solution. Rather than present the solution and explain it piece-by-piece, wouldn't it be more interesting to discover some of it? Let's start with some questions related to the problem at hand: Extending the JDT with our own method visibility refactoring capability.

Asking the right question is more important than knowing the answer
Our quest begins with some general questions:

Once we've got a good handle on the basic Eclipse landscape, we'll turn to some JDT-specific questions:

And of course, the final big question:

How and where will the extension be shown in the user interface?
This is mostly a gentle reminder, since we've already decided on the answer. We want to show context menu choices for one or more selected methods that allow us to change their visibility with a single action. We prefer that they be available wherever the methods can be displayed, such as the Hierarchy view and Package Explorer. This leads us to our next question.

How do we extend the user interface in general?
Learning by example is more fun, and this is where the Plug-in Project wizard can give us a hand by providing some sample code that we can then modify to our needs. We'll answer just a few of its questions and it will automatically launch the specialized perspective for plug-in development, known as the Plug-in Development Environment (PDE), ready for testing. This wizard includes a number of examples that will get us started. In fact, our old friend is there, "Hello World." Just for old time's sake, let's generate it, look at the result to verify that the environment is set up correctly, and then modify it to help us answer the current question and lead us to the next question: How does an extension to the user interface know about basic events like selection? That will be important, since we want to apply our newly introduced menu choices to the currently selected method(s).

Note that these instructions assume that you're starting from a fresh Eclipse installation. If you have modified the environment or changed preferences, they may not work precisely as described below. You might consider starting Eclipse with a fresh workspace by opening a Command Prompt window, changing to the <inst_dir>\eclipse directory, and starting Eclipse with the -data parameter, as shown in Listing 1.

Listing 1. Starting a fresh instance of Eclipse
      
cd c:\eclipse2.1\eclipse
eclipse.exe -data workspaceDevWorks
      

Begin by creating a plug-in project using the New Plug-in Project wizard. Select File > New > Project. In the New Project dialog, select Plug-in Development and Plug-in Project in the list of wizards, and then select Next. Name the project com.ibm.lab.helloworld. The wizard will create a plug-in id based on this name, so it must be unique in the system (by convention, the project name and the plug-in id are the same). The proposed default workspace location shown under "Project contents" is fine; select Next.

Accept the default plug-in project structure on the following page by selecting Next. The plug-in code generator page proposes a number of samples that the wizard can help you further parameterize. Select the "Hello, World" option and then select Next. The next page, shown in Figure 4, proposes a plug-in name and plug-in class name. These are based on the last word of the plug-in project, com.ibm.lab.helloworld . This example doesn't need any of the plug-in class convenience methods, so deselect the three code generation options, as shown in Figure 4 and select Next (not Finish; you've got one more page to go).

Figure 4. Simple plug-in content
Simple plug-in content

The next page, shown in Figure 5, is where you can specify parameters that are unique to the "Hello, World" example, such as the message that will be displayed.

Figure 5. Sample action set
Sample action set

To simplify the resulting code, change the target package name for the action from com.ibm.lab.helloworld.actions to com.ibm.lab.helloworld, the same name as the project. While you might choose to have separate packages for grouping related classes in a real world plug-in, in this case there will be only two classes, so there's no need. Plus that adheres to the convention that the "main" package is named the same as the project. Now select Finish.

You should see an information message saying "Plug-ins required to compile Java classes in this plug-in are currently disabled. The wizard will enable them to avoid compile errors." Select OK to continue. If this is a fresh workspace, you will also see another information message saying "This kind of project is associated with the Plug-in Development Perspective. Do you want to switch to this perspective now?" Select Yes to switch as the message suggests.

To verify that everything is set up correctly, let's test your new plug-in. Select Run > Run As > Run-Time Workbench. This will launch a second instance of Eclipse that will include your plug-in. This new instance will create a new workspace directory named runtime-workspace, so don't worry; whatever testing you do with that instance will not affect your development setup. You should see something like Figure 6 with a new menu pull-down labeled Sample Menu having a single choice, Sample Action. Selecting it will show the information message below. If you didn't start from a fresh workspace, you can select Window > Reset Perspective to see the newly contributed pull-down menu; it isn't shown when starting from an existing workspace since the Workbench "remembers" what action sets were active the last time Eclipse was running (you can also add / remove action sets from the Window > Customize Perspective... pull-down menu choice).

Figure 6. Hello, Eclipse world
Hello, Eclipse world

Let's take a quick glance at the plug-in manifest file, plugin.xml. Double-click it to open it in the Plug-in Manifest editor. This editor presents several wizard-like pages and a "raw" source page. Turn to it by selecting the Source tab. You'll see something like what's shown below in Listing 2; we're interested in the parts in bold.

Listing 2. Generated "Hello, World" plugin.xml

        <extension
    point="org.eclipse.ui.actionSets">>
  <actionSet
      label="Sample Action Set"
      visible="true"
      id="com.ibm.lab.helloworld.actionSet">
    <menu
        label="Sample &Menu"
        id="sampleMenu">
      <separator 
        name="sampleGroup">
      </separator>
    </menu>
    <action
        label="&Sample Action"
        icon="icons/sample.gif"
        class="com.ibm.lab.helloworld.SampleAction"
        tooltip="Hello, Eclipse world"
        menubarPath="sampleMenu/sampleGroup"
        toolbarPath="sampleGroup"
        id="com.ibm.lab.helloworld.SampleAction">
    </action>
  </actionSet>
</extension>
      

It isn't necessary to study this too closely. The purpose of Part II of our tour is only to familiarize you with some of the basic mechanisms whereby we can introduce our extensions to the JDT. Here you see a sample of one such technique to add menus and menu choices to the Workbench as an action set. It begins with an extension, declared with the <extension point="org.eclipse.ui.actionSets"> tag. The Workbench user interface plug-in defines this extension point, org.eclipse.ui.actionSets, and several others like it where other plug-ins can contribute to the various user interface elements.

We still haven't answered how we can add menu choices to the context menu of Java methods. A simple example can give us some hints. Begin by opening the class that displays the "Hello, World" message, SampleAction, and note its run method. It isn't particularly interesting; however, we also see another method, selectionChanged. Aha! The answer to our next question awaits.

How does an extension to the user interface know about basic events like selection?
Contributed actions, like our contributed menu pull-down choice, are notified when the Workbench selection changes. That's confirmed in the Javadoc comments before the method. Let's modify this method to tell us a bit more about the selection. First, if you haven't already closed the runtime instance of the Workbench, do so now. Then add the code in Listing 3 to the selectionChanged method.

Listing 3. selectionChanged method, first modification

public void selectionChanged(IAction action, ISelection selection) {
  System.out.println("==========> selectionChanged");
  System.out.println(selection);
}
      

With this debug code, we'll see what is selected and learn a little more about what makes Eclipse work. Save the method and relaunch the runtime Workbench.

Important: Eclipse has a deferred load strategy to avoid loading plug-ins until the user does something that requires their code. So you must first select the Sample Action menu choice to load your plug-in before your selectionChanged method will be called.

Now select different things like text in an editor, files in the Navigator, and, of course, members in the Outline view (recall that you'll have to create a Java project and an example Java class to do this, since the runtime instance uses a different workspace). Listing 4 shows some example output that you will see in the Console of the development instance of Eclipse.

Listing 4. selectionChanged output, first modification

==========> selectionChanged
[package com.ibm.lab.soln.jdt.excerpt [in [Working copy] ChangeIMemberFlagAction.java 
    [in com.ibm.lab.soln.jdt.excerpt [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]
==========> selectionChanged
<empty selection>
==========> selectionChanged
org.eclipse.jface.text.TextSelection@9fca283
==========> selectionChanged
<empty selection>
==========> selectionChanged
[package com.ibm.lab.soln.jdt.excerpt [in [Working copy] ChangeIMemberFlagAction.java 
    [in com.ibm.lab.soln.jdt.excerpt [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]
==========> selectionChanged
[IMember[] members [in ChangeIMemberFlagAction [in [Working copy] ChangeIMemberFlagAction.java 
    [in com.ibm.lab.soln.jdt.excerpt [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]]
==========> selectionChanged
<empty selection>
==========> selectionChanged
[ChangeIMemberFlagAction.java [in com.ibm.lab.soln.jdt.excerpt 
      [in src [in com.ibm.lab.soln.jdt.excerpt]]]
  package com.ibm.lab.soln.jdt.excerpt
  import org.eclipse.jdt.core.Flags
  import org.eclipse.jdt.core.IBuffer
  ...lines omitted...
    void selectionChanged(IAction, ISelection)]
==========> selectionChanged
[boolean isChecked(IAction, IMember) [in ToggleIMemberFinalAction 
    [in ToggleIMemberFinalAction.java [in com.ibm.lab.soln.jdt.excerpt 
    [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]]
      

Well, that isn't as enlightening as we'd hoped. Clearly the selection isn't something as primitive as an instance of String, but it isn't evident what classes are involved either, because these classes have clearly overridden their default toString method. We're not yet at the point where we can appreciate what information they are showing without a little more investigation. Returning to the selectionChanged method, browse the hierarchy of the interface of the selection parameter, ISelection. Its hierarchy reveals that there are not many general purpose subtype interfaces, just IStructuredSelection (for lists) and ITextSelection. We'll make the selectionChanged method a bit smarter by outputting the class that's selected. Modify the selectionChanged method as shown in Listing 5.

Listing 5. selectionChanged method, second modification

public void selectionChanged(IAction action, ISelection selection) {
  System.out.println("==========> selectionChanged");
  if (selection != null) {
    if (selection instanceof IStructuredSelection) {
      IStructuredSelection ss = (IStructuredSelection) selection;
      if (ss.isEmpty())
        System.out.println("<empty selection>");
      else
        System.out.println("First selected element is " + ss.getFirstElement().getClass());
    } else if (selection instanceof ITextSelection) {
      ITextSelection ts = (ITextSelection) selection;
      System.out.println("Selected text is <" + ts.getText() + ">");
    }
  } else {
    System.out.println("<empty selection>");
  }     
}
      

Again, remember to close the runtime instance and relaunch. Now when you select various elements of the user interface, is it far more revealing, as shown in Listing 6.

Listing 6. selectionChanged output, second modification

    selected some methods in the Outline view
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
<selection is empty>

    activated the Java editor
==========> selectionChanged
Selected text is <isChecked>
==========> selectionChanged
<selection is empty>

    selected same methods and classes, package in the Package Explorer
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceType
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.PackageFragment

    activated the Navigator view, selected some files, folders, and projects
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.File
==========> selectionChanged
<selection is empty>
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.File
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.Project
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.Folder
==========> selectionChanged
<selection is empty>

    reactivated the Package Explorer, 
    selected some classes and methods in JARs of reference libraries
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.JarPackageFragment
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.ClassFile
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.BinaryMethod
      

Specifically, we confirm that what we see in the user interface corresponds one-for-one with model classes of the JDT. Why we're seeing what appears to be models as selections and not lower-level primitives like strings and images is thanks to another Eclipse framework, called JFace. This framework maps between primitives like strings that the widgets close to the operating system expect and the higher-level model objects with which your code prefers to work. This article will only peripherally touch on this topic, since our stated goal is extending the JDT. The Resources section suggests other references on JFace that will broaden your understanding. This article will only cover what's necessary to understand the basics of our JDT extension.

Returning to the output, a particular selection result draws our attention: those corresponding to the selection of Java members in the user interface. They are repeated in Listing 7.

Listing 7. selectionChanged output, revisited

==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.BinaryMethod
      

The internal in the middle of the package name for these classes is a little disquieting. However, as you'll often find, Eclipse will have a public interface that corresponds to the (internal) implementation class, as is the case here. A quick class lookup reveals that these classes all implement a common set of interfaces that look promising, namely ISourceReference, IJavaElement, and especially IMember. Finally! Now we have what we had hoped to extend, leading us to the answer to our next question.

How do we extend the user interface of specific elements of the JDT, like members shown in the Outline view? Do we extend the view(s) or their underlying model?
Our simple "Hello, World" example showed that adding a menu choice requires just a few lines of XML in the plug-in manifest file (<extension point="org.eclipse.ui.actionSet">) and a class that handles the actual action (com.ibm.lab.helloworld.SampleAction). Adding actions to views' pull-down menu, the common editors' toolbar, and pop-up menus is nearly as straightforward. Contributed pop-up menus come in two flavors: those that are associated with a view alone and not selected objects (that is, the "default" pop-up menu that views often display when you right-click on their "whitespace"), and the more common variety, those that are associated with choices applying to the selected object(s). In our case, we want to target only specific selected objects, so we'll contribute what's called an action object contribution to their pop-up menu by defining an extension in the plug-in manifest (some of the identifiers below will be shortened to format better; they are denoted by '…'), as shown in Listing 8.

Listing 8. Modifier actions

<extension point="org.eclipse.ui.popupMenus">
  <objectContribution
      objectClass="org.eclipse.jdt.core.IMember"
      id="...imember">
      
    <menu
        label="Soln: Modifiers"
        path="group.reorganize"
        id="...imember.modifiers">
      <separator name="group1"/>
      <separator name="group2"/>
    </menu>
    
    <action
      label="Private"
      menubarPath="...imember.modifiers/group1"
      class="...jdt.excerpt.MakeIMemberPrivateAction"
      id="...imember.makeprivate">
    </action>
    
    <action
      label="Protected"
      menubarPath="...imember.modifiers/group1"
      class="...jdt.excerpt.MakeIMemberProtectedAction"
      id="...imember.makeprotected">
    </action>
    
    ...all menu choices not shown...
    
  </objectContribution>
</extension>
      

The extension point is named org.eclipse.ui.popupMenus, and as the name suggests, it defines contributions to pop-up menus appearing in the Workbench. This particular example will contribute only to specifically selected objects, those implementing the IMember interface (recall that as defined in the Java language specification, members include both methods and fields). Our investigation has paid off; we have the answer to our current question and we're almost ready to move to the next question.

Before doing so, note at this point that the pattern that we found for our simple "Hello, World" action example will repeat itself for other menu action contributions. That is, the class named in the class attribute will be notified of selection changes (by its selectionChanged method) and will also be notified when the user selects the menu choice (by its run method). The user interface portion of our tour is almost over; the harder part, effecting our desired change, lies ahead. There is only an observation or two to make before continuing, as stated in our next question.

What is the relationship between elements shown in the Package Explorer and the same elements shown in other views like the Outline view? Does our extension need to be aware of any differences between them or not?
You may have noticed that when you selected a method in the Outline view and the Hierarchy view, the class of the selected object was not always the same. For example, if you expanded the contents of a library (JAR file) in the Package Explorer, then selected a class or method, it was also not the same class as the same selection in the Java editor's Outline view. What's up with that?

Here we are observing the difference between those parts of the JDT's Java model that are "editable" versus those that are always read-only. Both parts of the Java model will implement a common interface, like IMember, but have different implementation classes that understand the underlying restrictions. As another example, there is an implementation class representing a Java compilation unit derived from a .class file in a JAR file shown in the Package Explorer and another class representing a compilation unit derived directly from a .java file. The latter implementation will allow modifications where the former cannot, yet a shared portion of their API is represented by the interface ICompilationUnit.

You have no doubt observed beforehand when editing Java source code that the Outline view updates as you're typing a method signature (if you haven't noticed, try it!). This is an example of how the JDT stages its "uncommitted changes" in a separate area versus those changes that have already been saved, compiled, and integrated into the Java model. Some views, like the Java editor's Outline view, are aware of both uncommitted changes and committed changes, while others like the Navigator view are only concerned with committed changes that are saved to the file system.

Subsequently, our contributed action that will modify Java members has to be, at least to some extent, aware of the context in which it is invoked. That is, it will have to recognize that some selected members are modifiable (those in the Java editor's Outline view) while others are not (members from .class file stored in a JAR file and shown in the Package Explorer). Keeping this in mind, let's continue on to our next question.

How do you change the JDT model programmatically?
If you explored a bit during the prior tour, you may have noticed that IMember, IJavaElement, and what appears to be the majority of the interfaces implemented by the selected Java-related items our action saw have no setXXX methods. So how do you modify them?

You'll find it is surprisingly easy, yet perhaps not intuitively obvious. The JDT's Java model is in most practical respects "read only". With the integrated cooperation of the Java compiler, changes to the underlying Java source of a given element are synchronized with the rest of the Java model. In effect, all you have to do is update the Java source, and the rest of the necessary model changes are propagated to whoever is dependent on them. For example, the JDT's indices are automatically updated whenever Java source / Java model changes occur so searches continue to work quickly, dependent classes are recompiled (as dictated by the Java build path specified in the project's properties), and so on.

That's a big relief! This point is why the Java model is key to plug-in integration: It provides a common shared in-memory model of the entire Java environment, its scope beginning from a project and continuing to all its referenced libraries, all without you having to worry about manipulating .java files, .class files, and .jar files in the file system. You can focus on the high-level model and let the JDT deal with many of those messy details.

Not yet convinced it is that easy? Listing 9 contains the diminutive snippet of code that is at the heart of this solution, extracted from the contribute action's run method and simplified slightly for readability:

Listing 9. selectionChanged method, diminutive solution

public void selectionChanged(IAction action, ISelection selection) {      
  IMember member = (IMember) 
      ((IStructuredSelection) selection).getFirstElement();
  ICompilationUnit cu = member.getCompilationUnit();

  if (cu.isWorkingCopy()) {
    IBuffer buffer = cu.getBuffer();
    buffer.replace(...);
    cu.reconcile();
  }
}  
      

Seems a bit anticlimactic, doesn't it? Your contributed action is given the selected member, you ask it for its parent container (the model of the Java .class or .java file, collectively referred to as a compilation unit in JDT parlance) because that's who manages the underlying source, verify that it is part of the "uncommitted" Java model (in other words, it is currently open in an editor), then modify the source code returned as a buffer. The IBuffer interface is similar to StringBuffer, the principal difference being that changing the buffer associated with a compilation unit updates the corresponding elements of the Java model. The final call to reconcile tells the JDT to notify other interested parties like the Package Explorer view that your model updates are ready for public consumption.

You no doubt noticed the ellipsis in the code above. That's where you have to analyze the source code itself to apply your modifications. Again, the JDT comes to your aid, as we'll see in the next question.

How do you analyze Java code to apply modifications?
The JDT offers several tools to help you analyze code. This article intentionally chose the easiest to demonstrate and the one with the most limited scope, the IScanner interface. This interface is part of the JDT toolbox and is accessible from the JDT's ToolFactory class. Its createScanner method returns a scanner that simplifies the tokenizing of a string of Java code. It isn't handling anything particularly difficult, just straightforward parsing and categorization of the returned tokens. For example, it indicates the next token is the public keyword, the token after that is an identifier, the token after that is an open parenthesis, and so on. Subsequently, this scanner is only appropriate when you want to analyze a small piece of code where you have a precise understanding of what to expect. You would never use a scanner to analyze an entire Java source; for that you would turn to something quite familiar to compiler aficionados: the JDT's Abstract Syntax Tree (AST) framework.

Unlike the simple scanner, an AST understands the relationships between language elements (they are no longer simply "tokens"). It can recognize something as a local variable, instance variable, expression, if statement -- over sixty different language elements. It will help you with refactoring that covers a wide scope or has particularly sticky ambiguities that defy a one-for-one categorization of tokens. To see more clearly this distinction of when you would use a scanner versus an AST, consider the code in Listing 10.

Listing 10. Ambiguous variable references

public class Foo {
  int foo = 1;
  
  public int foo(int foo) {
      return foo + this.foo;
  }
  
  public int getFoo() {
    return foo;
  }
}  
      

If you wanted to find references to the instance variable foo as part of your refactoring, you can see how a naïve parse would make it challenging to distinguish between local references and instance variable references. An AST creates a full analysis tree where each element of the Java source is represented and distinguished. In this particular case, the "foo" references would be represented as nodes of the AST by different classes that take their context into consideration, like FieldDeclaration, SimpleName, and ThisExpression, thereby making it easy for you to recognize which is which.

As mentioned earlier, this article will only cover the simple case we've chosen. For examples of more complex modifications and analyses, see the Resources section. Now let's return to the ellipsis code that we skipped earlier. This code will use an instance of IScanner to identify and replace the keyword(s) in the source that determine a member's visibility. The visibility modifiers that we'll handle are public, private, protected, and final. We can simplify the solution by taking a brute force approach, that is, doing it in two steps. First remove all visibility modifiers in the method signature (or at least scan for them and remove them if found), then insert the desired modifier. Specifically:

  1. Remove public, private, or protected if found in the method signature.
  2. Insert the requested visibility modifiers (in the case of package visibility, do nothing because it is the default; that is, there are no modifiers).

The final modifier is easy. Since the desired behavior is to toggle it on and off, we only have to remove it if it is present; otherwise, insert it. The code in Listing 11 will show just one case, unconditionally changing a member's visibility from public to private. In the solution associated with this article, you'll see that the common code for each of the actions has been moved to an abstract superclass. It is basically the same as the code below, just tidied up to eliminate redundancy.

Listing 11. Scanning for public keyword

ICompilationUnit cu = member.getCompilationUnit();

if (cu.isWorkingCopy()) {
  IBuffer buffer = cu.getBuffer();
  
  IScanner scanner =
    ToolFactory.createScanner(false, false, false, false);
  scanner.setSource(buffer.getCharacters());
  ISourceRange sr = member.getSourceRange();
  scanner.resetTo(
    sr.getOffset(),
    sr.getOffset() + sr.getLength() - 1);

  int token = scanner.getNextToken();
  while (token != ITerminalSymbols.TokenNameEOF
      && token != ITerminalSymbols.TokenNameLPAREN)
    token = scanner.getNextToken();

    if (token == ITerminalSymbols.TokenNamePUBLIC) {
      buffer.replace(
        scanner.getCurrentTokenStartPosition(),
        scanner.getCurrentTokenEndPosition(),
        scanner.getCurrentTokenStartPosition() + 1,
        "private");
      break;
    }
  }
  cu.reconcile();
}
      

Note: ITerminalSymbols defines the token names that a scanner can return, corresponding to the standard tokens of the Java grammar. You can further query the scanner to ask where specifically in the buffer the current token begins and ends, what line number it appears on, and, of course, the token itself (especially cases like ITerminalSymbols.TokenNameStringLiteral and ITerminalSymbols.TokenNameIdentifier that aren't reserved keywords).

In the code snippet above, the scanner.setSource method is given the entire source code for the compilation unit, that is, everything in the Java source file. As was mentioned earlier, the scanner isn't well suited to large analyses, so we must restrict it to only the source portion starting at the first character of the target method until its end by calling the setSourceRange method. The IMember interface is an extension of ISourceReference, an interface that allows you to query the source string and source location within the containing compilation unit. This saves us the trouble of having to figure out where our target method begins and ends within the Java source. We could have done just that with an AST, but the ISourceReference interface renders that unnecessary. Since Java method signatures are easy to parse, the parsing capability of the IScanner interface is a good match. All we have to do is look for a public keyword that appears between the first character of the method declaration and before the opening parenthesis of the parameter declaration, replacing it with the private keyword. Of course, in the solution it will handle all of the possible cases, whether the method was originally public, private, protected, or package (default).

Where do you go from here?
The stated goal of this article was to give you a non-trivial extension to Eclipse's Java development environment that enhanced its productivity. In all honesty, more than once I skipped over some details for reasons of brevity. The solution itself makes some simplifying assumptions, like only allowing modifications to Java source already open in the editor. You would probably want to relax that restriction in a fuller implementation.

Nonetheless, I hope that you got a good taste of what's possible and you're convinced that it isn't all too difficult. What we've covered in this article is a portion of one of the advanced chapters in our book, The Java Developer's Guide to Eclipse. Eleven less advanced chapters cover the fundamentals of plug-in development. Like this article, most chapters include a documented working solution that reinforces what you've learned, much in the same style as what you've seen in this article (though perhaps not covered at such a breakneck pace!).

In addition to our book, you'll find great articles on developerWorks and the home of Eclipse, eclipse.org -- a number of which are listed in the Resources section below.

Learning more about the solution and downloading it
Get more details about what was covered in this article in the solution excerpt (see Resources for a link). The solution excerpt also describes several other useful extensions to the JDT that are included on the CD-ROM accompanying The Java Developer's Guide to Eclipse. To install the solution excerpt, first download it, unzip the project contained in it to your workspace (for example, c:\eclipse2.1\eclipse\workspace), and then import the project into your current Eclipse workspace by selecting File > Import > Existing Project into Workspace.

Important: You may need to add required plug-ins to your workspace so the solution will compile and run. Select Window > Preferences > Plug-in Development > Target Platform and select Not in Workspace. This will assure that the base plug-ins upon which the solution relies will be available during the import and recompilation process.

Once it is imported, you will probably need to switch to the Plug-in Development perspective, select the plugin.xml in the com.ibm.lab.soln.jdt.excerpt project, and choose Update Classpath. This will correct compilation errors caused by differences between your Eclipse installation paths and the solution's.

Resources

  • Download the source code used in this article or first browse more details about it.

  • The eclipse.org Web site is the home of Eclipse.

  • This article's solution is based in part on the companion solution in Chapter 26 of The Java Developer's Guide to Eclipse, by Sherry Shavor, Jim D'Anjou, Dan Kehn, Scott Fairbrother, John Kellerman, and Pat McCarthy (Addison Wesley Professional, 2003; ISBN 0321159640).

  • Find more resources for Eclipse users on developerWorks.

About the author
Dan Kehn is a Senior Software Engineer at IBM in Research Triangle Park, North Carolina. His interest in object-oriented programming goes back to 1985, long before it enjoyed the acceptance it has today. He has a broad range of software experience, having worked on development tools like VisualAge for Smalltalk, operating system performance and memory analysis, and user interface design. Dan worked as a consultant for object-oriented development projects throughout the U.S. as well as for four years in Europe. His recent interests include object-oriented analysis/design, application development tools, and Web programming with the WebSphere Application Server. In May 2001 he joined the Eclipse Jumpstart team, which helps ISVs create commercial offerings based on the Eclipse Platform. He and the rest of the Jumpstart team authored The Java Developer's Guide to Eclipse from which the solution presented in this article was excerpted.


code709 KBe-mail it!

What do you think of this document?
Killer! (5) Good stuff (4) So-so; not bad (3) Needs work (2) Lame! (1)

Comments?



developerWorks > Open source projects | Java technology
developerWorks
  About IBM  |  Privacy  |  Terms of use  |  Contact