From 0300cded42a1b403f4caef76c5cc96460e0c056d Mon Sep 17 00:00:00 2001 From: aboyko Date: Wed, 15 May 2024 11:29:13 -0400 Subject: [PATCH] InlayHint to upgrade spring-boot-starter-parent to the latest patch --- .../boot/maven/PomInlayHintHandler.java | 165 +++++++++++++----- .../boot/maven/PomInlayHintHandlerTest.java | 94 ++++++++++ 2 files changed, 216 insertions(+), 43 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandler.java index 4fe8c999a..d699e3180 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandler.java @@ -12,21 +12,28 @@ package org.springframework.ide.vscode.boot.maven; import java.net.URI; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Supplier; +import java.util.stream.Collectors; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.dom.DOMElement; import org.eclipse.lemminx.dom.DOMParser; +import org.eclipse.lemminx.dom.DOMText; import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.InlayHint; import org.eclipse.lsp4j.InlayHintKind; import org.eclipse.lsp4j.InlayHintLabelPart; import org.eclipse.lsp4j.InlayHintParams; +import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.rewrite.SpringBootUpgrade; import org.springframework.ide.vscode.boot.validation.generations.GenerationsValidator; import org.springframework.ide.vscode.boot.validation.generations.SpringProjectsProvider; import org.springframework.ide.vscode.boot.validation.generations.VersionValidationUtils; @@ -39,6 +46,7 @@ import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFin import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; import org.springframework.ide.vscode.commons.languageserver.util.InlayHintHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.TextDocument; public class PomInlayHintHandler implements InlayHintHandler { @@ -95,72 +103,143 @@ public class PomInlayHintHandler implements InlayHintHandler { URI uri = URI.create(params.getTextDocument().getUri()); if ("file".equals(uri.getScheme()) && POM_XML.equals(Paths.get(uri).getFileName().toString())) { + List inlayHintProviders = new ArrayList<>(); + Optional projectOpt = projectFinder.find(params.getTextDocument()); if (projectOpt.isPresent()) { - URI buildFileUri = projectOpt.get().getProjectBuild().getBuildFile(); - - if (buildFileUri.equals(uri) && SpringProjectUtil.isBootProject(projectOpt.get())) { - Version currentVersion = SpringProjectUtil.getSpringBootVersion(projectOpt.get()); + IJavaProject jp = projectOpt.get(); + URI buildFileUri = jp.getProjectBuild().getBuildFile(); + + if (buildFileUri.equals(uri) && SpringProjectUtil.isBootProject(jp)) { + Version currentVersion = SpringProjectUtil.getSpringBootVersion(jp); if (currentVersion != null) { try { ResolvedSpringProject genProject = generationsProvider.getProject(SpringProjectUtil.SPRING_BOOT); if (genProject != null) { - Generation generation = GenerationsValidator.getGenerationForJavaProject(projectOpt.get(), genProject); - if (generation != null && VersionValidationUtils.isOssValid(generation)) { - TextDocument doc = server.getTextDocumentService().getLatestSnapshot(params.getTextDocument().getUri()); - - if (doc != null) { - String content = doc.get(); + Version latestPatch = VersionValidationUtils.getNewerLatestPatchRelease(genProject.getReleases(), currentVersion); + if (latestPatch != null) { + inlayHintProviders.add(new InlayHintWithLazyPosition(() -> { + Command command = new Command(); + command.setTitle("Upgrade to the Latest Patch"); + command.setCommand(SpringBootUpgrade.CMD_UPGRADE_SPRING_BOOT); + command.setArguments(List.of(jp.getLocationUri().toASCIIString(), latestPatch.toString(), false)); - if (!content.isEmpty()) { - // if doc is not empty, dive into the details and provide more sophisticated content assist proposals - DOMParser parser = DOMParser.getInstance(); - DOMDocument dom = parser.parse(content, "", null); - - DOMElement project = dom.getDocumentElement(); - if (project != null && "project".equals(project.getTagName())) { - for (int j = 0; j < project.getChildNodes().getLength(); j++) { - var child = project.getChildNodes().item(j); - if ("dependencies".equals(child.getNodeName()) && child instanceof DOMElement dependencies) { - try { - Command command = new Command(); - command.setTitle("Add Spring Boot Starters"); - command.setCommand("spring.initializr.addStarters"); - - InlayHintLabelPart label = new InlayHintLabelPart("Add Spring Boot Starters..."); - label.setCommand(command); - - InlayHint hint = new InlayHint(); - hint.setPosition(doc.toPosition(dependencies.getStartTagCloseOffset() + 1)); - hint.setKind(InlayHintKind.Parameter); - hint.setPaddingLeft(true); - hint.setLabel(List.of(label)); - - return List.of(hint); - } catch (Exception e) { - log.error("", e); - break; - } + InlayHintLabelPart label = new InlayHintLabelPart("Upgrade to the Latest Patch"); + label.setCommand(command); + + InlayHint hint = new InlayHint(); + hint.setKind(InlayHintKind.Parameter); + hint.setPaddingLeft(true); + hint.setLabel(List.of(label)); + return hint; + }, (d, e) -> { + Optional parentArtifactIdOpt = findChildElement(e, 0, "parent", "artifactId") + .flatMap(PomInlayHintHandler::getNodeValue); + if (parentArtifactIdOpt.isPresent() && "spring-boot-starter-parent".equals(parentArtifactIdOpt.get())) { + Optional parentVersionOpt = findChildElement(e, 0, "parent", "version"); + if (parentVersionOpt.isPresent()) { + DOMElement parentVersion = parentVersionOpt.get(); + // Get the current version in the POM in case file is not saved + Optional parentVersionValueOpt = getNodeValue(parentVersion).flatMap(s -> Optional.ofNullable(Version.parse(s))); + if (parentVersionValueOpt.isPresent() && parentVersionValueOpt.get().compareTo(latestPatch) < 0) { + try { + return List.of(d.toPosition(parentVersion.getEndTagCloseOffset() + 1)); + } catch (Exception ex) { + log.error("", ex); } } } - - } - } + return Collections.emptyList(); + })); + } + Generation generation = GenerationsValidator.getGenerationForJavaProject(jp, genProject); + if (generation != null && VersionValidationUtils.isOssValid(generation)) { + + inlayHintProviders.add(new InlayHintWithLazyPosition(() -> { + Command command = new Command(); + command.setTitle("Add Spring Boot Starters"); + command.setCommand("spring.initializr.addStarters"); + + InlayHintLabelPart label = new InlayHintLabelPart("Add Spring Boot Starters..."); + label.setCommand(command); + + InlayHint hint = new InlayHint(); + hint.setKind(InlayHintKind.Parameter); + hint.setPaddingLeft(true); + hint.setLabel(List.of(label)); + return hint; + }, (d, e) -> { + Optional dependenciesOpt = findChildElement(e, 0, "dependencies"); + if (dependenciesOpt.isPresent()) { + DOMElement dependencies = dependenciesOpt.get(); + try { + return List.of(d.toPosition(dependencies.getStartTagCloseOffset() + 1)); + } catch (BadLocationException ex) { + log.error("", ex); + } + } + return Collections.emptyList(); + })); } } } catch (Exception e) { log.error("", e); } + } + } + + + } + + if (!inlayHintProviders.isEmpty()) { + TextDocument doc = server.getTextDocumentService().getLatestSnapshot(params.getTextDocument().getUri()); + + if (doc != null) { + String content = doc.get(); + if (!content.isEmpty()) { + // if doc is not empty, dive into the details and provide more sophisticated content assist proposals + DOMParser parser = DOMParser.getInstance(); + DOMDocument dom = parser.parse(content, "", null); + DOMElement project = dom.getDocumentElement(); + + if (project != null && "project".equals(project.getTagName())) { + return inlayHintProviders.stream().flatMap(provider -> provider.computeInlayHints(doc, project).stream()).collect(Collectors.toList()); + } } } } - } return Collections.emptyList(); } + + private static Optional findChildElement(DOMElement e, int idx, String... tagPath) { + if (tagPath.length == idx) { + return Optional.of(e); + } + for (int j = 0; j < e.getChildNodes().getLength(); j++) { + var child = e.getChildNodes().item(j); + if (tagPath[idx].equals(child.getNodeName()) && child instanceof DOMElement c) { + return findChildElement(c, idx + 1, tagPath); + } + } + return Optional.empty(); + } + + private static Optional getNodeValue(DOMElement e) { + return e.getChildren().stream().filter(DOMText.class::isInstance).map(DOMText.class::cast).map(tn -> tn.getData().trim()).findFirst(); + } + + private record InlayHintWithLazyPosition(Supplier hintFactory, BiFunction> positionSupplier) { + List computeInlayHints(TextDocument d, DOMElement e) { + return positionSupplier.apply(d, e).stream().map(p -> { + InlayHint hint = hintFactory.get(); + hint.setPosition(p); + return hint; + }).collect(Collectors.toList()); + } + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandlerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandlerTest.java index 9c22ee29a..bdaf67139 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandlerTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandlerTest.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.Test; import org.springframework.ide.vscode.boot.validation.generations.SpringProjectsProvider; import org.springframework.ide.vscode.boot.validation.generations.json.Generation; import org.springframework.ide.vscode.boot.validation.generations.json.ResolvedSpringProject; +import org.springframework.ide.vscode.commons.Version; import org.springframework.ide.vscode.commons.java.SpringProjectUtil; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; @@ -135,5 +136,98 @@ public class PomInlayHintHandlerTest { assertEquals(0, hints.size()); } + + @Test + void upgradePatchVersionInlay() throws Exception { + MavenJavaProject jp = projects.mavenProject("empty-boot-15-web-app"); + + TextDocument doc = new TextDocument(jp.getProjectBuild().getBuildFile().toASCIIString(), LanguageId.XML, 0, Files.readString(Paths.get(jp.getProjectBuild().getBuildFile()))); + + JavaProjectFinder projectFinder = mock(JavaProjectFinder.class); + when(projectFinder.find(any())).thenReturn(Optional.of(jp)); + + SimpleTextDocumentService documents = mock(SimpleTextDocumentService.class); + when(documents.getLatestSnapshot(anyString())).thenReturn(doc); + + SimpleLanguageServer server = mock(SimpleLanguageServer.class); + when(server.getTextDocumentService()).thenReturn(documents); + + ResolvedSpringProject resolvedProject = mock(ResolvedSpringProject.class); + when(resolvedProject.getGenerations()).thenReturn(null); + when(resolvedProject.getSlug()).thenReturn(SpringProjectUtil.SPRING_BOOT); + when(resolvedProject.getReleases()).thenReturn(List.of( + Version.parse("1.5.6"), + Version.parse("1.5.7"), + Version.parse("1.5.8"), + Version.parse("1.5.9"), + Version.parse("1.5.10"), + Version.parse("2.0.0") + )); + + SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); + when(projectProvider.getProject(SpringProjectUtil.SPRING_BOOT)).thenReturn(resolvedProject); + + PomInlayHintHandler inlayHanlder = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + + List hints = inlayHanlder.handle(mock(CancelChecker.class), new InlayHintParams(new TextDocumentIdentifier(doc.getUri()), doc.toRange(0, doc.getLength()))); + + assertEquals(1, hints.size()); + + InlayHint hint = hints.get(0); + + assertEquals(new Position(17, 34), hint.getPosition()); + + assertTrue(hint.getLabel().isRight()); + + assertEquals(1, hint.getLabel().getRight().size()); + + InlayHintLabelPart labelPart = hint.getLabel().getRight().get(0); + + assertEquals("Upgrade to the Latest Patch", labelPart.getValue()); + + Command cmd = labelPart.getCommand(); + + assertNotNull(cmd); + + assertEquals("sts/upgrade/spring-boot", cmd.getCommand()); + assertEquals(jp.getLocationUri().toASCIIString(), cmd.getArguments().get(0)); + assertEquals("1.5.10", cmd.getArguments().get(1)); + + } + + @Test + void upgradePatchVersionInlay_AlreadyOnLatestPatch() throws Exception { + MavenJavaProject jp = projects.mavenProject("empty-boot-15-web-app"); + + TextDocument doc = new TextDocument(jp.getProjectBuild().getBuildFile().toASCIIString(), LanguageId.XML, 0, Files.readString(Paths.get(jp.getProjectBuild().getBuildFile()))); + + JavaProjectFinder projectFinder = mock(JavaProjectFinder.class); + when(projectFinder.find(any())).thenReturn(Optional.of(jp)); + + SimpleTextDocumentService documents = mock(SimpleTextDocumentService.class); + when(documents.getLatestSnapshot(anyString())).thenReturn(doc); + + SimpleLanguageServer server = mock(SimpleLanguageServer.class); + when(server.getTextDocumentService()).thenReturn(documents); + + ResolvedSpringProject resolvedProject = mock(ResolvedSpringProject.class); + when(resolvedProject.getGenerations()).thenReturn(null); + when(resolvedProject.getSlug()).thenReturn(SpringProjectUtil.SPRING_BOOT); + when(resolvedProject.getReleases()).thenReturn(List.of( + Version.parse("1.5.6"), + Version.parse("1.5.7"), + Version.parse("1.5.8"), + Version.parse("2.0.0") + )); + + SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); + when(projectProvider.getProject(SpringProjectUtil.SPRING_BOOT)).thenReturn(resolvedProject); + + PomInlayHintHandler inlayHanlder = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + + List hints = inlayHanlder.handle(mock(CancelChecker.class), new InlayHintParams(new TextDocumentIdentifier(doc.getUri()), doc.toRange(0, doc.getLength()))); + + assertEquals(0, hints.size()); + } }