|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| What is possible, where to start, and how to
proceed
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
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
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
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
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
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
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:
- Remove
public , private , or
protected if found in the method signature.
- 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. |
|
|