Monday, February 15, 2010

A rose by any other name...

Some years ago when I wrote about Eclipse's APIs for supporting automated refactorings, it wasn't all too convenient to drive an existing JDT refactoring from your own plug-in. For instance, if you had an IField or an IMethod in your hands (one of JDT's Java language model objects), you couldn't simply call some API methods to rename that thing (field or method). You had to write quite a bit of code, and it was still difficult to avoid Eclipse presenting your refactoring in the standard wizard dialog. A few days ago I was playing around with a new fun feature in Usus (I'm not telling what it is :-) — and I found it much more convenient to do such a thing nowadays.

How you do it

The central object that represents a refactoring procedure is the Language Toolkit's Refactoring (unsurprisingly). If you want to use an existing automated refactoring, such as Rename field or Extract method from the JDT pool, your first step is to retrieve an object that conforms to the Refactoring contract.

You get one of these guys from the refactoring core plugin: first, add a plug-in dependency to org.eclipse.ltk.core.refactoring. You can then call the static factory method RefactoringCore.getRefactoringContribution( String id )in order to retrieve a refactoring contribution, i.e. an automated refactoring that was contributed from some plug-in via an extension in the plugin.xml. For this you need to know the ID of that refactoring contribution. In the case of the JDT refactorings, use the constants in IJavaRefactorings (add a dependency to org.eclipse.jdt.core.manipulation). You can now feed the refactoring contribution with the parameters it needs; in the case of a Rename Method refactoring, for instance, that would be the IMethod object that represents the method we want to rename, and the new name we to give to it. Having done that, we can ask the contribution to create the Refactoring object for us. In sum, you'd do something like this:


private Refactoring mkRefactoring( RefactoringStatus status, IMethod methodToRename, String newName ) throws CoreException {
RenameJavaElementDescriptor desc = loadDescriptor();
desc.setJavaElement( methodToRename );
desc.setNewName( newName );
return desc.createRefactoring( status );
}

private RenameJavaElementDescriptor loadDescriptor() {
String id = IJavaRefactorings.RENAME_METHOD;
RefactoringContribution contrib = RefactoringCore.getRefactoringContribution( id );
return (RenameJavaElementDescriptor)contrib.createDescriptor();
}



As you can see, I'm passing a RefactoringStatus object into the creation operation. That object will contain the result of the creation (which might be an OK or some error status).

Once you are in possession of a refactoring object, you might run in it the standard refactoring dialog (in order to be nice to the user and present them with a preview and the cancel option), or you could extract the Change objects from it and just ask them to perform their work. All in all, a quick test drive of a Rename Method refactoring might be something along these lines:


RefactoringStatus status = new RefactoringStatus();
Refactoring refactoring = mkRefactoring( status, methodToRename, newName );
if( status.isOK() ) {
if( refactoring.checkAllConditions( nullMonitor() ).isOK() ) {
Change change = refactoring.createChange( nullMonitor() );
change.perform( nullMonitor() );
}
}

// ...

private IProgressMonitor nullMonitor() {
return new NullProgressMonitor();
}



A word to the wise

Obviously, there are many aspects you want to consider if you try this in production code (are there editors open which you'll want to save? what about undo operations? do you want to support the local history? ...). You might want to look at the JDT code more in depth in order to find out how to handle these things. (That's something I didn't because I merely wanted to play around with them.) Have fun!

1 comment: