Vulcan - Exposing Eclipse JDT Programmatically
Published: 2024-05-22 03:16:22

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](https://github.com/feroult/vulcan/blob/main/refactor/application.bndrun).

```properties
# 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)'
```

2. **Add Dependencies in `pom.xml`**: Ensure all required OSGi bundles are included. You can find the complete `pom.xml` file [here](https://github.com/feroult/vulcan/blob/main/refactor/pom.xml).

```xml
<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](https://github.com/feroult/vulcan/blob/main/refactor/src/main/java/vulcan/utils/ProjectUtils.java).

```java
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](https://github.com/feroult/vulcan/blob/main/refactor/src/main/java/vulcan/refactorings/BrowseInvoker.java).

```java
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](https://github.com/feroult/vulcan/blob/main/refactor/src/main/java/vulcan/refactorings/core/RenameType.java).

```java
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](https://github.com/feroult/vulcan).

Cheers!