Skip to content

Commit d33ab5d

Browse files
committed
Add an updateMavenProject tool for "Maven > Update Project"
Editing a pom.xml had no way to take effect: until the project configuration is updated, the workspace does not see the change, so a newly added dependency is not on the classpath and everything using it still fails to compile. The only way through was for a human to run Maven > Update Project by hand. Expose m2e's IProjectConfigurationManager.updateProjectConfiguration with a MavenUpdateRequest - the same thing the IDE action runs - taking the project plus the forceDependencyUpdate ("Force Update of Snapshots/Releases") and offline flags. Marked longExecution: resolving dependencies can reach the network, and the tool joins with the operation's cancellable monitor rather than a NullProgressMonitor, so a slow update can be polled and aborted.
1 parent 8244df1 commit d33ab5d

3 files changed

Lines changed: 118 additions & 0 deletions

File tree

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/servers/EclipseIntegrationsMcpServer.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,19 @@ public String runMavenBuild(
290290
Optional.ofNullable(timeout).map(Integer::parseInt).orElse(0));
291291
}
292292

293+
@Tool(name = "updateMavenProject", longExecution = true,
294+
description = "Runs the equivalent of the IDE's 'Maven > Update Project' action: re-reads the pom, re-resolves dependencies and reconfigures the project's classpath. Use this after editing a pom.xml - until it runs, the workspace does not see the change, so a newly added dependency is not on the classpath and code using it still fails to compile.",
295+
type = "object")
296+
public String updateMavenProject(
297+
@ToolParam(name = "projectName", description = "The name of the Maven project to update (use listMavenProjects to find it)", required = true) String projectName,
298+
@ToolParam(name = "forceDependencyUpdate", description = "If 'true', re-resolves snapshots and releases even when already cached (the 'Force Update of Snapshots/Releases' checkbox). Default: false", required = false) String forceDependencyUpdate,
299+
@ToolParam(name = "offline", description = "If 'true', resolves only from the local repository without reaching the network. Default: false", required = false) String offline)
300+
{
301+
boolean force = Optional.ofNullable(forceDependencyUpdate).map(Boolean::parseBoolean).orElse(false);
302+
boolean workOffline = Optional.ofNullable(offline).map(Boolean::parseBoolean).orElse(false);
303+
return mavenService.updateMavenProject(projectName, force, workOffline);
304+
}
305+
293306
@Tool(name = "getEffectivePom", longExecution = true, description = "Gets the effective POM for a Maven project.", type = "object")
294307
public String getEffectivePom(
295308
@ToolParam(name = "projectName", description = "The name of the Maven project", required = true) String projectName)

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/services/MavenService.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import org.eclipse.core.runtime.IProgressMonitor;
1919
import org.eclipse.core.runtime.IStatus;
2020

21+
import org.eclipse.m2e.core.project.MavenUpdateRequest;
22+
2123
import com.github.gradusnikov.eclipse.assistai.mcp.operations.Operation;
2224
import com.github.gradusnikov.eclipse.assistai.mcp.operations.OperationContext;
2325
import org.eclipse.core.runtime.NullProgressMonitor;
@@ -38,6 +40,12 @@
3840
@Creatable
3941
public class MavenService
4042
{
43+
/**
44+
* m2e's Maven nature. Hardcoded rather than taken from IMavenConstants, which lives in
45+
* an internal package.
46+
*/
47+
private static final String MAVEN_NATURE_ID = "org.eclipse.m2e.core.maven2Nature";
48+
4149

4250
@Inject
4351
ILog logger;
@@ -229,6 +237,76 @@ private void executeMavenBuild( IMavenProjectFacade facade, List<String> goals,
229237
MavenPlugin.getProjectConfigurationManager().updateProjectConfiguration( facade.getProject(), monitor );
230238
}
231239

240+
/**
241+
* The headless equivalent of the IDE's "Maven > Update Project" action: re-reads the
242+
* pom, re-resolves dependencies and reconfigures the project's classpath and facets.
243+
* <p>
244+
* This is what a pom edit needs before the workspace reflects it - without it, a newly
245+
* added dependency is simply not on the classpath and everything that uses it still
246+
* fails to compile.
247+
*
248+
* @param projectName the Maven project to update
249+
* @param forceDependencyUpdate re-resolve SNAPSHOT and release dependencies even when
250+
* they are already cached (the "Force Update of
251+
* Snapshots/Releases" checkbox)
252+
* @param offline work offline, resolving only from the local repository
253+
*/
254+
public String updateMavenProject( String projectName, boolean forceDependencyUpdate, boolean offline )
255+
{
256+
Objects.requireNonNull( projectName, "Project name cannot be null" );
257+
258+
if ( projectName.isEmpty() )
259+
{
260+
throw new RuntimeException( "Error: Project name cannot be empty." );
261+
}
262+
263+
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject( projectName );
264+
if ( !project.exists() )
265+
{
266+
throw new RuntimeException( "Error: Project '" + projectName + "' does not exist." );
267+
}
268+
if ( !project.isOpen() )
269+
{
270+
throw new RuntimeException( "Error: Project '" + projectName + "' is closed." );
271+
}
272+
273+
try
274+
{
275+
if ( !project.hasNature( MAVEN_NATURE_ID ) )
276+
{
277+
throw new RuntimeException( "Error: Project '" + projectName + "' is not a Maven project." );
278+
}
279+
280+
// Cancellable, unlike a NullProgressMonitor: resolving dependencies can reach
281+
// the network and take a while, so the caller must be able to abort it.
282+
IProgressMonitor monitor = OperationContext.current()
283+
.map( Operation::monitor )
284+
.map( IProgressMonitor.class::cast )
285+
.orElseGet( NullProgressMonitor::new );
286+
287+
MavenUpdateRequest request = new MavenUpdateRequest( project, offline, forceDependencyUpdate );
288+
MavenPlugin.getProjectConfigurationManager().updateProjectConfiguration( request, monitor );
289+
290+
StringBuilder sb = new StringBuilder();
291+
sb.append( "Updated Maven project '" ).append( projectName ).append( "'." );
292+
if ( forceDependencyUpdate )
293+
{
294+
sb.append( " Dependencies were re-resolved." );
295+
}
296+
if ( offline )
297+
{
298+
sb.append( " Resolved offline, from the local repository only." );
299+
}
300+
sb.append( "\n\nUse getCompilationErrors to check the project after the update." );
301+
return sb.toString();
302+
}
303+
catch ( CoreException e )
304+
{
305+
logger.error( "Error updating Maven project " + projectName, e );
306+
throw new RuntimeException( "Error updating Maven project '" + projectName + "': " + e.getMessage(), e );
307+
}
308+
}
309+
232310
/**
233311
* Gets the effective POM for a Maven project.
234312
*

tests/com.github.gradusnikov.eclipse.plugin.assistai.main.tests/src/com/github/gradusnikov/eclipse/plugin/assistai/mcp/services/MavenServiceTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,33 @@ public void afterEach() throws CoreException {
167167
}
168168
}
169169

170+
@Test
171+
public void testUpdateMavenProject_ValidProject() {
172+
try {
173+
// Offline, so the test does not depend on the network.
174+
String result = service.updateMavenProject(TEST_PROJECT_NAME, false, true);
175+
176+
assertTrue(result.contains("Updated Maven project"), result);
177+
assertTrue(result.contains(TEST_PROJECT_NAME), result);
178+
} catch (RuntimeException e) {
179+
String message = String.valueOf(e.getMessage());
180+
if (message.contains("is not a Maven project") || message.contains("Could not find Maven configuration")) {
181+
// m2e configuration is not always available in the test environment.
182+
assumeTrue(false, "Skipping: Maven configuration not available (" + message + ")");
183+
}
184+
throw e;
185+
}
186+
}
187+
188+
@Test
189+
public void testUpdateMavenProject_InvalidProject() {
190+
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
191+
service.updateMavenProject("NonExistentProject", false, false);
192+
});
193+
194+
assertTrue(exception.getMessage().contains("does not exist"), exception.getMessage());
195+
}
196+
170197
@Test
171198
public void testRunMavenBuild_InvalidProject() {
172199
// Test with non-existent project

0 commit comments

Comments
 (0)