Vulcan - Exposing Eclipse JDT Programmatically
Fernando Ultremare
Posted on May 22, 2024
Hey Dev.to community,
I wanted to share a project I worked on a while back called Vulcan. It's a tool that lets you use Eclipse JDT (Java Development Tools) programmatically via command line or REST API, without needing to run the Eclipse workbench.
What is Vulcan?
Vulcan is designed to expose the Eclipse JDT OSGi bundle in a programmatic way. It allows you to perform various refactorings such as:
- Extract Method: Extracts a block of code into a new method.
- Rename Type: Renames a class or interface.
- Rename Method: Renames a method.
- Rename Field: Renames a field.
- Rename Local Variable: Renames a local variable.
- Chained Refactorings: Allows multiple refactorings to be applied in sequence.
Why I Built It
I used Vulcan to automate refactorings before AI started taking over these tasks. It was super handy for improving code quality and maintaining consistency across projects. With all the new AI advances in refactoring, Vulcan has become a bit obsolete. But I learned a ton while working on it, and it was a fun project.
How It Works
Setting Up OSGi
Setting up the OSGi environment involves selecting and finding the necessary packages from the p2 repository. Here’s how you can do it:
-
Create a
bndrun
File: This file specifies the OSGi bundles and their versions. You can find the completebndrun
file here.
# refactor/application.bndrun
index: target/index.xml;name="app"
-standalone: ${index}
-runfw: org.eclipse.osgi
-runproperties.equinox: osgi.console=, osgi.console.enable.builtin=true, osgi.ws=gtk, osgi.os=linux, osgi.arch=x86_64
-runee: JavaSE-17
-runproperties: \
osgi.instance.area=../../../../vulcan-test-workspace,\
jetty.home.bundle=org.eclipse.jetty.osgi.boot
-runrequires: \
bnd.identity;id=org.eclipse.jdt.core,\
bnd.identity;id=org.eclipse.jdt.ui,\
bnd.identity;id=vulcan-refactor
-runbundles: \
org.eclipse.jdt.core;version='[3.35.0,3.35.1)',\
org.eclipse.jdt.ui;version='[3.30.0,3.30.1)',\
vulcan-refactor;version='[1.0.0,1.0.1)'
-
Add Dependencies in
pom.xml
: Ensure all required OSGi bundles are included. You can find the completepom.xml
file here.
<dependencies>
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>org.eclipse.jdt.core</artifactId>
<version>3.35.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>org.eclipse.jdt.ui</artifactId>
<version>3.30.0</version>
</dependency>
<!-- Add other dependencies as needed -->
</dependencies>
Setting Up the Workspace
To set up the workspace, you need to create or open an existing workspace and load the project. You can find the complete implementation here.
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
public class ProjectUtils {
public static IJavaProject getJavaProject(String projectName) throws CoreException {
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
if (!project.exists()) {
project.create(null);
}
project.open(null);
IJavaProject javaProject = JavaCore.create(project);
return javaProject;
}
}
Accessing Project Information
Once the project is loaded, you can access information like source directories and classpaths. You can find the complete implementation here.
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaModelException;
public class ProjectInfo {
public static void printProjectInfo(IJavaProject javaProject) throws JavaModelException {
System.out.println("Source folders:");
Arrays.stream(javaProject.getPackageFragmentRoots())
.filter(root -> root.getKind() == IPackageFragmentRoot.K_SOURCE)
.forEach(root -> System.out.println(root.getPath()));
System.out.println("Classpath entries:");
Arrays.stream(javaProject.getRawClasspath())
.forEach(entry -> System.out.println(entry.getPath()));
}
}
Handling Internals
Some refactorings do not expose a public API, and you may need to dig into internals. For example, to perform a rename refactoring, you might need to use internal classes and methods. You can find the complete implementation here.
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.refactoring.descriptors.RenameJavaElementDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringContribution;
import org.eclipse.ltk.core.refactoring.RefactoringCore;
public class RenameTypeRefactoring {
public RenameJavaElementDescriptor createRenameDescriptor(IType type, String newName) {
RefactoringContribution contribution = RefactoringCore.getRefactoringContribution(IJavaRefactorings.RENAME_TYPE);
RenameJavaElementDescriptor descriptor = (RenameJavaElementDescriptor) contribution.createDescriptor();
descriptor.setProject(type.getJavaProject().getElementName());
descriptor.setJavaElement(type);
descriptor.setNewName(newName);
descriptor.setUpdateReferences(true);
return descriptor;
}
}
Conclusion
I decided to share Vulcan now in case anyone finds it interesting or useful. Check it out on GitHub: Vulcan.
Cheers!
Posted on May 22, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.