Skip to content

Commit

Permalink
InlayHint to upgrade spring-boot-starter-parent to the latest patch
Browse files Browse the repository at this point in the history
  • Loading branch information
BoykoAlex committed May 15, 2024
1 parent ce0fcce commit 0300cde
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,28 @@

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;
Expand All @@ -39,6 +46,7 @@
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 {
Expand Down Expand Up @@ -95,72 +103,143 @@ public List<InlayHint> handle(CancelChecker token, InlayHintParams params) {
URI uri = URI.create(params.getTextDocument().getUri());
if ("file".equals(uri.getScheme()) && POM_XML.equals(Paths.get(uri).getFileName().toString())) {

List<InlayHintWithLazyPosition> inlayHintProviders = new ArrayList<>();

Optional<IJavaProject> 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<String> parentArtifactIdOpt = findChildElement(e, 0, "parent", "artifactId")
.flatMap(PomInlayHintHandler::getNodeValue);
if (parentArtifactIdOpt.isPresent() && "spring-boot-starter-parent".equals(parentArtifactIdOpt.get())) {
Optional<DOMElement> 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<Version> 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<DOMElement> 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<DOMElement> 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<String> 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<InlayHint> hintFactory, BiFunction<TextDocument, DOMElement, List<Position>> positionSupplier) {
List<InlayHint> 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());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
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;
Expand Down Expand Up @@ -135,5 +136,98 @@ void inlayNotProvidedOutOfOssSupport() throws Exception {
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<InlayHint> 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<InlayHint> hints = inlayHanlder.handle(mock(CancelChecker.class), new InlayHintParams(new TextDocumentIdentifier(doc.getUri()), doc.toRange(0, doc.getLength())));

assertEquals(0, hints.size());
}

}

0 comments on commit 0300cde

Please sign in to comment.