- What is possible, where to start, and how to proceed
- Your own easy refactoring of member visibility
- A brief tour of "Hello World"
- Asking the right question is more important than knowing the answer
- Where do you go from here?
- Learning more about the solution and downloading it
- About the author
Asking the right question is more important than knowing the answer
Our quest begins with some general questions:
-
How and where will the extension be shown in the user interface?
-
How does an extension to the user interface know about basic events like selection?
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).