Vulcan - Exposing Eclipse JDT Programmatically

feroult

Fernando Ultremare

Posted on May 22, 2024

Vulcan - Exposing Eclipse JDT Programmatically

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:

  1. Create a bndrun File: This file specifies the OSGi bundles and their versions. You can find the complete bndrun 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)'
Enter fullscreen mode Exit fullscreen mode
  1. Add Dependencies in pom.xml: Ensure all required OSGi bundles are included. You can find the complete pom.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>
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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()));
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

I decided to share Vulcan now in case anyone finds it interesting or useful. Check it out on GitHub: Vulcan.

Cheers!

💖 💪 🙅 🚩
feroult
Fernando Ultremare

Posted on May 22, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related