diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/Version.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/Version.java index 60559b391f..de701d0a44 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/Version.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/Version.java @@ -54,6 +54,15 @@ public String toMajorMinorVersionStr() { return sb.toString(); } + public String toMajorMinorPatchVersionStr() { + StringBuilder sb = new StringBuilder(); + sb.append(major); + sb.append('.'); + sb.append(minor); + sb.append('.'); + sb.append(patch); + return sb.toString(); + } @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java index 9c09255143..4866c51a1b 100644 --- a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java @@ -316,8 +316,11 @@ public static List parse(JavaParser parser, Iterable sour }); } - public static List parseInputs(JavaParser parser, List inputs, Consumer parseCallback) { - ExecutionContext ctx = new InMemoryExecutionContext(ORAstUtils::logExceptionWhileParsing); + public static ExecutionContext createDefaultContext() { + return new InMemoryExecutionContext(ORAstUtils::logExceptionWhileParsing); + } + + public static List parseInputs(JavaParser parser, ExecutionContext ctx, List inputs, Consumer parseCallback) { ctx.putMessage(JavaParser.SKIP_SOURCE_SET_TYPE_GENERATION, true); ctx.putMessage(ExecutionContext.REQUIRE_PRINT_EQUALS_INPUT, false); if (parseCallback != null) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java index efb18bc219..44b5877422 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2024 Pivotal, Inc. + * Copyright (c) 2018, 2026 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -38,6 +38,7 @@ import org.springframework.ide.vscode.boot.maven.PomLanguageServerComponents; import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider; import org.springframework.ide.vscode.boot.properties.BootPropertiesLanguageServerComponents; +import org.springframework.ide.vscode.boot.validation.generations.MavenMetadataProvider; import org.springframework.ide.vscode.boot.validation.generations.SpringProjectsProvider; import org.springframework.ide.vscode.boot.xml.SpringXMLLanguageServerComponents; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -130,7 +131,7 @@ public void afterPropertiesSet() throws Exception { new BootJavaLanguageServerComponents(appContext), new SpringXMLLanguageServerComponents(server, springIndexer, params, config, appContext.getBean(SpelReconciler.class)), new SpringFactoriesLanguageServerComponents(projectFinder, springIndexer, config), - new PomLanguageServerComponents(server, projectFinder, params.projectObserver, appContext.getBean(SpringProjectsProvider.class)), + new PomLanguageServerComponents(server, projectFinder, params.projectObserver, appContext.getBean(SpringProjectsProvider.class), appContext.getBean(MavenMetadataProvider.class)), new JpaQueryPropertiesLanguageServerComponents(server.getTextDocumentService(), projectFinder, appContext.getBean(JpqlSemanticTokens.class), appContext.getBean(HqlSemanticTokens.class), appContext.getBean(JpqlSupportState.class), (Reconciler) appContext.getBean("hqlReconciler"), (Reconciler) appContext.getBean("jpqlReconciler")) ); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootVersionValidationConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootVersionValidationConfig.java index a833a48685..8047d8b16a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootVersionValidationConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootVersionValidationConfig.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.app; +import java.net.URI; import java.util.List; import java.util.Optional; @@ -23,6 +24,7 @@ import org.springframework.ide.vscode.boot.java.rewrite.SpringBootUpgrade; import org.springframework.ide.vscode.boot.validation.BootVersionValidationEngine; import org.springframework.ide.vscode.boot.validation.generations.GenerationsValidator; +import org.springframework.ide.vscode.boot.validation.generations.MavenMetadataProvider; import org.springframework.ide.vscode.boot.validation.generations.ProjectVersionDiagnosticProvider; import org.springframework.ide.vscode.boot.validation.generations.SpringCloudCompatibilityValidator; import org.springframework.ide.vscode.boot.validation.generations.SpringIoProjectsProvider; @@ -39,8 +41,12 @@ public class BootVersionValidationConfig { private static final Logger log = LoggerFactory.getLogger(BootVersionValidationConfig.class); - @Bean UpdateBootVersion updateBootVersion(SimpleLanguageServer server, Optional bootUpgradeOpt, SpringProjectsProvider projectsProvider) { - return new UpdateBootVersion(server.getDiagnosticSeverityProvider(), bootUpgradeOpt, projectsProvider); + @Bean MavenMetadataProvider mavenMetadataProvider(SimpleLanguageServer server) { + return new MavenMetadataProvider(server.getWorkspaceService().getFileObserver()); + } + + @Bean UpdateBootVersion updateBootVersion(SimpleLanguageServer server, Optional bootUpgradeOpt, SpringProjectsProvider projectsProvider, MavenMetadataProvider mavenMetadataProvider) { + return new UpdateBootVersion(server.getDiagnosticSeverityProvider(), bootUpgradeOpt, projectsProvider, mavenMetadataProvider); } @Bean SpringIoProjectsProvider springProjectsProvider(SimpleLanguageServer server, BootJavaConfig config, RestTemplateFactory restTemplateFactory) { @@ -64,7 +70,7 @@ public class BootVersionValidationConfig { @Bean ProjectReconcileScheduler bootVersionValidationScheduler(SimpleLanguageServer server, JavaProjectFinder projectFinder, BootJavaConfig config, ProjectObserver projectObserver, - ProjectVersionDiagnosticProvider diagnosticProvider) { + ProjectVersionDiagnosticProvider diagnosticProvider, MavenMetadataProvider mavenMetadataProvider) { return new ProjectReconcileScheduler(server, new BootVersionValidationEngine(server, config, projectObserver, projectFinder, diagnosticProvider), projectFinder) { @@ -73,6 +79,20 @@ ProjectReconcileScheduler bootVersionValidationScheduler(SimpleLanguageServer se protected void init() { super.init(); config.addListener(evt -> scheduleValidationForAllProjects()); + + mavenMetadataProvider.addListener(uriStr -> { + try { + URI uri = URI.create(uriStr); + for (IJavaProject project : projectFinder.all()) { + if (project.getProjectBuild() != null && uri.equals(project.getProjectBuild().getBuildFile())) { + scheduleValidation(project); + } + } + } catch (Exception e) { + log.error("Failed to process build file change for URI: " + uriStr, e); + } + }); + projectObserver.addListener(new ProjectObserver.Listener() { @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java index 072fede989..8dce1c24a1 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -30,6 +31,7 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.WorkspaceEditChangeAnnotationSupportCapabilities; +import org.openrewrite.ExecutionContext; import org.openrewrite.InMemoryExecutionContext; import org.openrewrite.ParseExceptionResult; import org.openrewrite.Parser; @@ -40,7 +42,9 @@ import org.openrewrite.config.Environment; import org.openrewrite.internal.InMemoryLargeSourceSet; import org.openrewrite.java.JavaParser; +import org.openrewrite.maven.MavenExecutionContextView; import org.openrewrite.maven.MavenParser; +import org.openrewrite.maven.MavenSettings; import org.openrewrite.tree.ParseError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -109,18 +113,27 @@ public CompletableFuture> getRecipe(String name) { return recipes().thenApply(recipes -> Optional.ofNullable(recipes.get(name))); } + private MavenExecutionContextView createContext(Consumer onError) { + MavenExecutionContextView ctx = MavenExecutionContextView.view(new InMemoryExecutionContext(onError)); + MavenSettings settings = MavenSettings.readMavenSettingsFromDisk(ctx); + String[] profiles = settings.getActiveProfiles() == null ? new String[0] : settings.getActiveProfiles().getActiveProfiles().toArray(String[]::new); + ctx.setMavenSettings(settings, profiles); + return ctx; + } + @SuppressWarnings("unchecked") CompletableFuture applyToBuildFiles(Recipe r, String uri, String progressToken, boolean askForPreview) { return projectFinder.find(new TextDocumentIdentifier(uri)).map(p -> { final IndefiniteProgressTask progressTask = server.getProgressService().createIndefiniteProgressTask(progressToken, r.getDisplayName(), "Initiated..."); final IJavaProject project = p; + ExecutionContext ctx = createContext(e -> log.error("Project Parsing error:", e)); return CompletableFuture.supplyAsync(() -> { Path absoluteProjectDir = Paths.get(project.getLocationUri()); progressTask.progressEvent("Parsing files..."); ProjectParser projectParser = getProjectParser(project); - return (List) projectParser.parseBuildFiles(absoluteProjectDir, new InMemoryExecutionContext(e -> log.error("Project Parsing error:", e))); + return (List) projectParser.parseBuildFiles(absoluteProjectDir, ctx); }) - .thenCompose(sources -> computeWorkspaceEditAwareOfPreview(r, sources, progressTask, askForPreview)) + .thenCompose(sources -> computeWorkspaceEditAwareOfPreview(r, ctx, sources, progressTask, askForPreview)) .thenCompose(we -> applyEdit(we, progressTask, r.getDisplayName())) .whenComplete((o,t) -> progressTask.done()); }).orElse(CompletableFuture.failedFuture(new IllegalArgumentException("Cannot find Spring Boot project for uri: " + uri))); @@ -136,9 +149,10 @@ CompletableFuture apply(Recipe r, String uri, String progressToken, bool Path absoluteProjectDir = Paths.get(project.getLocationUri()); progressTask.progressEvent("Parsing files..."); ProjectParser projectParser = getProjectParser(project); - List sources = projectParser.parse(absoluteProjectDir, new InMemoryExecutionContext(e -> log.error("Project Parsing error:", e))); + ExecutionContext ctx = createContext(e -> log.error("Project Parsing error:", e)); + List sources = projectParser.parse(absoluteProjectDir, ctx); - return computeWorkspaceEditAwareOfPreview(r, sources, progressTask, askForPreview) + return computeWorkspaceEditAwareOfPreview(r, ctx, sources, progressTask, askForPreview) .thenCompose(we -> applyEdit(we, progressTask, r.getDisplayName())); } else { return CompletableFuture.failedFuture(new IllegalArgumentException("Cannot find Spring Boot project for uri: " + uri)); @@ -146,9 +160,9 @@ CompletableFuture apply(Recipe r, String uri, String progressToken, bool }).whenComplete((o,t) -> progressTask.done()); } - CompletableFuture> computeWorkspaceEditAwareOfPreview(Recipe r, List sources, IndefiniteProgressTask progressTask, boolean askForPreview) { + CompletableFuture> computeWorkspaceEditAwareOfPreview(Recipe r, ExecutionContext ctx, List sources, IndefiniteProgressTask progressTask, boolean askForPreview) { String changeAnnotationId = UUID.randomUUID().toString(); - Optional we = computeWorkspaceEdit(r, sources, progressTask, changeAnnotationId); + Optional we = computeWorkspaceEdit(r, ctx, sources, progressTask, changeAnnotationId); if (we.isPresent()) { return askForPreview ? askForPreview(we.get(), changeAnnotationId) : CompletableFuture.completedFuture(we); } @@ -204,12 +218,12 @@ private CompletableFuture applyEdit(Optional we, Indefini } } - Optional computeWorkspaceEdit(Recipe r, List sources, IndefiniteProgressTask progressTask, String changeAnnotationId) { + Optional computeWorkspaceEdit(Recipe r, ExecutionContext ctx, List sources, IndefiniteProgressTask progressTask, String changeAnnotationId) { reportParseErrors(sources.stream().filter(ParseError.class::isInstance).map(ParseError.class::cast).collect(Collectors.toList())); if (progressTask != null) { progressTask.progressEvent("Computing changes..."); } - RecipeRun reciperun = r.run(new InMemoryLargeSourceSet(sources), new InMemoryExecutionContext(e -> log.error("Recipe execution failed", e))); + RecipeRun reciperun = r.run(new InMemoryLargeSourceSet(sources), ctx); List results = reciperun.getChangeset().getAllResults(); return ORDocUtils.createWorkspaceEdit(server.getTextDocumentService(), results, changeAnnotationId).map(we -> { ChangeAnnotation changeAnnotation = new ChangeAnnotation(r.getDisplayName()); @@ -236,7 +250,7 @@ private void reportParseErrors(List parseErrors) { private static ProjectParser createRewriteProjectParser(IJavaProject jp, Function inputProvider) { switch (jp.getProjectBuild().getType()) { case ProjectBuild.MAVEN_PROJECT_TYPE: - MavenParser.Builder mavenParserBuilder = MavenParser.builder(); + MavenParser.Builder mavenParserBuilder = MavenParser.builder().skipDependencyResolution(true); return new MavenIJavaProjectParser(jp, JavaParser.fromJavaVersion(), inputProvider, mavenParserBuilder); default: throw new IllegalStateException("The project is neither Maven nor Gradle!"); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java index f4e2c96e75..c3d2bf708f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java @@ -30,6 +30,7 @@ import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceEdit; +import org.openrewrite.ExecutionContext; import org.openrewrite.Parser.Input; import org.openrewrite.Recipe; import org.openrewrite.SourceFile; @@ -126,16 +127,17 @@ private CompletableFuture> perform(FixDescriptor data) { log.warn("Code Action failed to resolve. Could not create recipe created with id '" + data.getRecipeId() + "'."); } List cus = new ArrayList<>(); + ExecutionContext ctx = ORAstUtils.createDefaultContext(); if (projectWide) { JavaParser jp = ORAstUtils.createJavaParserBuilder(project.get()).dependsOn(data.getTypeStubs()).build(); List inputs = ORAstUtils.getParserInputs(server.getTextDocumentService(), project.get()); - cus.addAll(ORAstUtils.parseInputs(jp, inputs, null)); + cus.addAll(ORAstUtils.parseInputs(jp, ctx, inputs, null)); } else { JavaParser jp = ORAstUtils.createJavaParserBuilder(project.get()).dependsOn(data.getTypeStubs()).build(); List inputs = data.getDocUris().stream().map(URI::create).map(Paths::get).map(p -> ORAstUtils.getParserInput(server.getTextDocumentService(), p)).filter(Objects::nonNull).collect(Collectors.toList()); - cus.addAll(ORAstUtils.parseInputs(jp, inputs, null)); + cus.addAll(ORAstUtils.parseInputs(jp, ctx, inputs, null)); } - return recipeRepo.computeWorkspaceEditAwareOfPreview(r, cus, progress, projectWide).whenComplete((o, t) -> progress.done()); + return recipeRepo.computeWorkspaceEditAwareOfPreview(r, ctx, cus, progress, projectWide).whenComplete((o, t) -> progress.done()); }).exceptionally(t -> { progress.done(); return Optional.empty(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java index 3efe9be09c..381cabf9c2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/SpringBootUpgrade.java @@ -69,8 +69,8 @@ private Recipe createUpgradeRecipe(Version version, Version targetVersion) { if (version.getMajor() == targetVersion.getMajor() && version.getMinor() == targetVersion.getMinor()) { // patch version upgrade - treat as pom versions only upgrade - recipe.getRecipeList().add(new org.openrewrite.maven.UpgradeDependencyVersion("org.springframework.boot", "*", version.getMajor() + "." + version.getMinor() + ".x", null, null, null)); - recipe.getRecipeList().add(new UpgradeParentVersion("org.springframework.boot", "spring-boot-starter-parent", version.getMajor() + "." + version.getMinor() + ".x", null, null)); + recipe.getRecipeList().add(new org.openrewrite.maven.UpgradeDependencyVersion("org.springframework.boot", "*", targetVersion.toMajorMinorPatchVersionStr(), null, null, null)); + recipe.getRecipeList().add(new UpgradeParentVersion("org.springframework.boot", "spring-boot-starter-parent", targetVersion.toMajorMinorPatchVersionStr(), null, null)); } if (recipe.getRecipeList().isEmpty()) { 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 10bc10323d..d283dbdd77 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 @@ -35,6 +35,9 @@ 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.MavenMetadata; +import org.springframework.ide.vscode.boot.validation.generations.MavenMetadataProvider; +import org.springframework.ide.vscode.boot.validation.generations.SortedVersions; import org.springframework.ide.vscode.boot.validation.generations.SpringProjectsProvider; import org.springframework.ide.vscode.boot.validation.generations.VersionValidationUtils; import org.springframework.ide.vscode.boot.validation.generations.json.Generation; @@ -58,10 +61,12 @@ public class PomInlayHintHandler implements InlayHintHandler { final private JavaProjectFinder projectFinder; final private SpringProjectsProvider generationsProvider; + final private MavenMetadataProvider mavenMetadataProvider; - public PomInlayHintHandler(SimpleLanguageServer server, JavaProjectFinder projectFinder, ProjectObserver projectObserver, SpringProjectsProvider generationsProvider) { + public PomInlayHintHandler(SimpleLanguageServer server, JavaProjectFinder projectFinder, ProjectObserver projectObserver, SpringProjectsProvider generationsProvider, MavenMetadataProvider mavenMetadataProvider) { this.projectFinder = projectFinder; this.generationsProvider = generationsProvider; + this.mavenMetadataProvider = mavenMetadataProvider; projectObserver.addListener(new ProjectObserver.Listener() { @@ -125,9 +130,25 @@ public List handle(TextDocument doc, Range range, CancelChecker cance } try { + SortedVersions versions = null; + if (org.springframework.ide.vscode.commons.protocol.java.ProjectBuild.MAVEN_PROJECT_TYPE.equals(jp.getProjectBuild().getType())) { + try { + MavenMetadata metadata = mavenMetadataProvider.getMetadata(jp, "org.springframework.boot", "spring-boot"); + if (metadata != null) { + versions = metadata.getReleaseVersions(); + } + } catch (Exception e) { + // Logged in provider, fallback will happen below + } + } + ResolvedSpringProject genProject = generationsProvider.getProject(SpringProjectUtil.SPRING_BOOT); - if (genProject != null) { - Version latestPatch = VersionValidationUtils.getNewerLatestPatchRelease(genProject.getReleases(), currentVersion); + if (versions == null && genProject != null) { + versions = new SortedVersions(genProject.getReleases()); + } + + if (versions != null) { + Version latestPatch = versions.getNewerLatestPatchRelease(currentVersion).orElse(null); if (latestPatch != null) { inlayHintProviders.add(new InlayHintWithLazyPosition(() -> { Command command = new Command(); @@ -170,34 +191,37 @@ public List handle(TextDocument doc, Range range, CancelChecker cance return Collections.emptyList(); })); } - Generation generation = GenerationsValidator.getGenerationForJavaProject(jp, genProject, genProject.getSlug()); - 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); + + if (genProject != null) { + Generation generation = GenerationsValidator.getGenerationForJavaProject(jp, genProject, genProject.getSlug()); + 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(); - })); + return Collections.emptyList(); + })); + } } } } catch (Exception e) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomLanguageServerComponents.java index 2c317a6fe6..68ce8a0ed0 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomLanguageServerComponents.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Broadcom, Inc. + * Copyright (c) 2024, 2026 Broadcom, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -25,8 +25,8 @@ public class PomLanguageServerComponents implements LanguageServerComponents{ private PomInlayHintHandler inlayHintHandler; - public PomLanguageServerComponents(SimpleLanguageServer server, JavaProjectFinder projectFinder, ProjectObserver projectObserver, SpringProjectsProvider generationsProvider) { - this.inlayHintHandler = new PomInlayHintHandler(server, projectFinder, projectObserver, generationsProvider); + public PomLanguageServerComponents(SimpleLanguageServer server, JavaProjectFinder projectFinder, ProjectObserver projectObserver, SpringProjectsProvider generationsProvider, org.springframework.ide.vscode.boot.validation.generations.MavenMetadataProvider mavenMetadataProvider) { + this.inlayHintHandler = new PomInlayHintHandler(server, projectFinder, projectObserver, generationsProvider, mavenMetadataProvider); } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/MavenMetadata.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/MavenMetadata.java new file mode 100644 index 0000000000..3e4dfeb87d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/MavenMetadata.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2026 Broadcom + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.validation.generations; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.Version; + +public class MavenMetadata { + + private static final Logger log = LoggerFactory.getLogger(MavenMetadata.class); + + private final SortedVersions releaseVersions; + + public MavenMetadata(org.openrewrite.maven.tree.MavenMetadata rawMetadata) { + List releases = new ArrayList<>(); + + if (rawMetadata != null && rawMetadata.getVersioning() != null && rawMetadata.getVersioning().getVersions() != null) { + for (String vStr : rawMetadata.getVersioning().getVersions()) { + try { + Version v = Version.parse(vStr); + if (isRelease(v)) { + releases.add(v); + } + } catch (Exception e) { + log.warn("Failed to parse %s".formatted(vStr), e); + } + } + } + + this.releaseVersions = new SortedVersions(releases); + } + + private boolean isRelease(Version v) { + String qualifier = v.getQualifier(); + return qualifier == null || qualifier.isEmpty() || "RELEASE".equalsIgnoreCase(qualifier); + } + + public SortedVersions getReleaseVersions() { + return releaseVersions; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/MavenMetadataProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/MavenMetadataProvider.java new file mode 100644 index 0000000000..f71b61a73e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/MavenMetadataProvider.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2026 Broadcom + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.validation.generations; + +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.SourceFile; +import org.openrewrite.maven.MavenExecutionContextView; +import org.openrewrite.maven.MavenParser; +import org.openrewrite.maven.MavenSettings; +import org.openrewrite.maven.internal.MavenPomDownloader; +import org.openrewrite.maven.tree.GroupArtifact; +import org.openrewrite.maven.tree.MavenResolutionResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.util.ListenerList; +import org.springframework.ide.vscode.commons.util.FileObserver; + +public class MavenMetadataProvider { + + private static final Logger log = LoggerFactory.getLogger(MavenMetadataProvider.class); + + private final Map cache = new ConcurrentHashMap<>(); + private final ListenerList listeners = new ListenerList<>(); + + public MavenMetadataProvider(FileObserver fileObserver) { + if (fileObserver != null) { + fileObserver.onAnyChange(List.of("**/pom.xml"), files -> { + for (String file : files) { + Path pom = Paths.get(URI.create(file)); + cache.keySet().removeIf(key -> key.startsWith(pom.toString())); + listeners.fire(file); + } + }); + } + } + + public void addListener(Consumer listener) { + listeners.add(listener); + } + + private MavenMetadata compute(Path pomPath, String groupId, String artifactId) { + try { + if (!Files.exists(pomPath)) { + return null; + } + + ExecutionContext ctx = new InMemoryExecutionContext(); + MavenExecutionContextView mvnCtx = MavenExecutionContextView.view(ctx); + MavenSettings settings = mvnCtx.getSettings(); + if (settings == null) { + mvnCtx.setMavenSettings(MavenSettings.readMavenSettingsFromDisk(ctx)); + } + + MavenParser parser = MavenParser.builder().skipDependencyResolution(true).build(); + + List parsed = parser.parse(Collections.singletonList(pomPath), pomPath.getParent(), ctx) + .collect(Collectors.toList()); + + if (parsed.isEmpty()) { + return null; + } + + MavenResolutionResult mrr = parsed.get(0).getMarkers().findFirst(MavenResolutionResult.class).orElse(null); + if (mrr == null) { + return null; + } + + MavenExecutionContextView mctx = MavenExecutionContextView.view(ctx); + Optional maybeSettings = Optional.ofNullable(mctx.effectiveSettings(mrr)); + List activeProfiles = maybeSettings.map(MavenSettings::getActiveProfiles) + .map(MavenSettings.ActiveProfiles::getActiveProfiles).orElse(null); + return new MavenMetadata(new MavenPomDownloader(mrr.getProjectPoms(), ctx, maybeSettings.orElse(null), + activeProfiles) + .downloadMetadata(new GroupArtifact(groupId, artifactId), null, mrr.getPom().getRepositories())); + } catch (Exception e) { + log.warn("Failed to fetch Maven metadata for {}:{} in pom {}", groupId, artifactId, pomPath, e); + return null; + } + } + + public MavenMetadata getMetadata(IJavaProject javaProject, String groupId, String artifactId) { + URI buildFileUri = javaProject.getProjectBuild() == null ? null : javaProject.getProjectBuild().getBuildFile(); + if (buildFileUri == null || !buildFileUri.getPath().endsWith("pom.xml")) { + return null; + } + Path pom = Paths.get(buildFileUri); + return cache.computeIfAbsent(getKey(pom, groupId, artifactId), k -> compute(pom, groupId, artifactId)); + } + + private String getKey(Path pom, String groupId, String artifactId) { + return pom.toString() + "|" + groupId + ":" + artifactId; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/SortedVersions.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/SortedVersions.java new file mode 100644 index 0000000000..e8f95543c4 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/SortedVersions.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2026 Broadcom + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.validation.generations; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.ide.vscode.commons.Version; + +public class SortedVersions { + + private final List descendingVersions; + + public SortedVersions(Collection versions) { + this.descendingVersions = versions.stream() + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + } + + public Optional getNewerLatestPatchRelease(Version current) { + return descendingVersions.stream() + .filter(v -> v.getMajor() == current.getMajor() + && v.getMinor() == current.getMinor() + && v.getPatch() > current.getPatch()) + .findFirst(); + } + + public Optional getNewerLatestMinorRelease(Version current) { + return descendingVersions.stream() + .filter(v -> v.getMajor() == current.getMajor() + && v.getMinor() > current.getMinor()) + .findFirst(); + } + + public Optional getNewerLatestMajorRelease(Version current) { + return descendingVersions.stream() + .filter(v -> v.getMajor() > current.getMajor()) + .findFirst(); + } + + public List toList() { + return descendingVersions; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/UpdateBootVersion.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/UpdateBootVersion.java index fce33b9bb0..ad8770a11e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/UpdateBootVersion.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/UpdateBootVersion.java @@ -39,20 +39,49 @@ public class UpdateBootVersion extends AbstractDiagnosticValidator { private Optional bootUpgradeOpt; private SpringProjectsProvider springProjectsProvider; + private MavenMetadataProvider mavenMetadataProvider; - public UpdateBootVersion(DiagnosticSeverityProvider diagnosticSeverityProvider, Optional bootUpgradeOpt, SpringProjectsProvider springProjectsProvider) { + public UpdateBootVersion(DiagnosticSeverityProvider diagnosticSeverityProvider, Optional bootUpgradeOpt, SpringProjectsProvider springProjectsProvider, MavenMetadataProvider mavenMetadataProvider) { super(diagnosticSeverityProvider); this.bootUpgradeOpt = bootUpgradeOpt; this.springProjectsProvider = springProjectsProvider; + this.mavenMetadataProvider = mavenMetadataProvider; } @Override public Collection validate(IJavaProject javaProject, Version javaProjectVersion) throws Exception { - List versions = springProjectsProvider.getProject(SpringProjectUtil.SPRING_BOOT).getReleases(); + SortedVersions versions = null; + + if (ProjectBuild.MAVEN_PROJECT_TYPE.equals(javaProject.getProjectBuild().getType())) { + try { + MavenMetadata metadata = mavenMetadataProvider.getMetadata(javaProject, "org.springframework.boot", "spring-boot"); + if (metadata != null) { + versions = metadata.getReleaseVersions(); + } + } catch (Exception e) { + // Logged in provider, fallback will happen below + } + } + + if (versions == null) { + List rawReleases = springProjectsProvider.getProject(SpringProjectUtil.SPRING_BOOT).getReleases(); + versions = new SortedVersions(rawReleases); + } + ImmutableList.Builder builder = ImmutableList.builder(); - validateMajorVersion(javaProject, javaProjectVersion, versions).ifPresent(builder::add); - validateMinorVersion(javaProject, javaProjectVersion, versions).ifPresent(builder::add); - validatePatchVersion(javaProject, javaProjectVersion, versions).ifPresent(builder::add); + + versions.getNewerLatestMajorRelease(javaProjectVersion) + .flatMap(latest -> validateMajorVersion(javaProject, javaProjectVersion, latest)) + .ifPresent(builder::add); + + versions.getNewerLatestMinorRelease(javaProjectVersion) + .flatMap(latest -> validateMinorVersion(javaProject, javaProjectVersion, latest)) + .ifPresent(builder::add); + + versions.getNewerLatestPatchRelease(javaProjectVersion) + .flatMap(latest -> validatePatchVersion(javaProject, javaProjectVersion, latest)) + .ifPresent(builder::add); + return builder.build(); } @@ -60,105 +89,71 @@ private boolean canProvideQuickfix(IJavaProject jp) { return ProjectBuild.MAVEN_PROJECT_TYPE.equals(jp.getProjectBuild().getType()); } - private Optional validateMajorVersion(IJavaProject javaProject, Version javaProjectVersion, List sortedBootVersions) { - Version latest = VersionValidationUtils.getNewerLatestMajorRelease(sortedBootVersions, javaProjectVersion); - - if (latest != null) { - VersionValidationProblemType problemType = VersionValidationProblemType.UPDATE_LATEST_MAJOR_VERSION; - - StringBuffer message = new StringBuffer(); - message.append("Newer major version of Spring Boot available: "); - message.append(latest.toString()); - - List actions = new ArrayList<>(2); - - if (canProvideQuickfix(javaProject)) { - bootUpgradeOpt.flatMap(bu -> bu.getNearestAvailableMinorVersion(latest)).map(targetVersion -> { - Version upgradeVersion = Version.parse(targetVersion); - if (javaProjectVersion.compareTo(upgradeVersion) >= 0) { - return null; - } - CodeAction c = new CodeAction(); - c.setKind(CodeActionKind.QuickFix); - c.setTitle("Upgrade to Spring Boot " + targetVersion + " (executes the full project conversion recipe from OpenRewrite)"); - String commandId = SpringBootUpgrade.CMD_UPGRADE_SPRING_BOOT; - c.setCommand(new Command("Upgrade to Version " + targetVersion, commandId, - ImmutableList.of(javaProject.getLocationUri().toASCIIString(), targetVersion, true))); - return c; - }).ifPresent(actions::add); - } - - actions.add(openReleaseNotesCodeAction(latest)); - - return Optional.ofNullable(createDiagnostic(actions, problemType, message.toString())); + private Optional validateMajorVersion(IJavaProject javaProject, Version javaProjectVersion, Version latest) { + List actions = new ArrayList<>(2); + + if (canProvideQuickfix(javaProject)) { + bootUpgradeOpt.flatMap(bu -> bu.getNearestAvailableMinorVersion(latest)).map(targetVersion -> { + Version upgradeVersion = Version.parse(targetVersion); + if (javaProjectVersion.compareTo(upgradeVersion) >= 0) { + return null; + } + CodeAction c = new CodeAction(); + c.setKind(CodeActionKind.QuickFix); + c.setTitle("Upgrade to Spring Boot " + targetVersion + " (executes the full project conversion recipe from OpenRewrite)"); + String commandId = SpringBootUpgrade.CMD_UPGRADE_SPRING_BOOT; + c.setCommand(new Command("Upgrade to Version " + targetVersion, commandId, + ImmutableList.of(javaProject.getLocationUri().toASCIIString(), targetVersion, true))); + return c; + }).ifPresent(actions::add); } - return Optional.empty(); - } + + actions.add(openReleaseNotesCodeAction(latest)); - private Optional validateMinorVersion(IJavaProject javaProject, Version javaProjectVersion, List sortedBootVersions) { - Version latest = VersionValidationUtils.getNewerLatestMinorRelease(sortedBootVersions, javaProjectVersion); - - if (latest != null) { - VersionValidationProblemType problemType = VersionValidationProblemType.UPDATE_LATEST_MINOR_VERSION; - - StringBuffer message = new StringBuffer(); - message.append("Newer minor version of Spring Boot available: "); - message.append(latest.toString()); - - List actions = new ArrayList<>(2); + return Optional.ofNullable(createDiagnostic(actions, VersionValidationProblemType.UPDATE_LATEST_MAJOR_VERSION, "Newer major version of Spring Boot available: %s".formatted(latest.toString()).toString())); + } - if (canProvideQuickfix(javaProject)) { - bootUpgradeOpt.flatMap(bu -> bu.getNearestAvailableMinorVersion(latest)).map(targetVersion -> { - Version upgradeVersion = Version.parse(targetVersion); - if (javaProjectVersion.compareTo(upgradeVersion) >= 0) { - return null; - } - CodeAction c = new CodeAction(); - c.setKind(CodeActionKind.QuickFix); - c.setTitle("Upgrade to Spring Boot " + targetVersion + " (executes the full project conversion recipe from OpenRewrite)"); - String commandId = SpringBootUpgrade.CMD_UPGRADE_SPRING_BOOT; - c.setCommand(new Command("Upgrade to Version " + targetVersion, commandId, - ImmutableList.of(javaProject.getLocationUri().toASCIIString(), targetVersion, true))); - return c; - }).ifPresent(actions::add); - } - - actions.add(openReleaseNotesCodeAction(latest)); - - return Optional.ofNullable(createDiagnostic(actions, problemType, message.toString())); + private Optional validateMinorVersion(IJavaProject javaProject, Version javaProjectVersion, Version latest) { + List actions = new ArrayList<>(2); + + if (canProvideQuickfix(javaProject)) { + bootUpgradeOpt.flatMap(bu -> bu.getNearestAvailableMinorVersion(latest)).map(targetVersion -> { + Version upgradeVersion = Version.parse(targetVersion); + if (javaProjectVersion.compareTo(upgradeVersion) >= 0) { + return null; + } + CodeAction c = new CodeAction(); + c.setKind(CodeActionKind.QuickFix); + c.setTitle("Upgrade to Spring Boot " + targetVersion + " (executes the full project conversion recipe from OpenRewrite)"); + String commandId = SpringBootUpgrade.CMD_UPGRADE_SPRING_BOOT; + c.setCommand(new Command("Upgrade to Version " + targetVersion, commandId, + ImmutableList.of(javaProject.getLocationUri().toASCIIString(), targetVersion, true))); + return c; + }).ifPresent(actions::add); } - return Optional.empty(); + + actions.add(openReleaseNotesCodeAction(latest)); + + return Optional.ofNullable(createDiagnostic(actions, VersionValidationProblemType.UPDATE_LATEST_MINOR_VERSION, "Newer minor version of Spring Boot available: %s".formatted(latest.toString()))); } - private Optional validatePatchVersion(IJavaProject javaProject, Version javaProjectVersion, List sortedBootVersions) { - Version latest = VersionValidationUtils.getNewerLatestPatchRelease(sortedBootVersions, javaProjectVersion); - - if (latest != null) { - VersionValidationProblemType problemType = VersionValidationProblemType.UPDATE_LATEST_PATCH_VERSION; - - StringBuffer message = new StringBuffer(); - message.append("Newer patch version of Spring Boot available: "); - message.append(latest.toString()); - - List actions = new ArrayList<>(2); - - if (canProvideQuickfix(javaProject)) { - bootUpgradeOpt.map(bu -> { - CodeAction c = new CodeAction(); - c.setKind(CodeActionKind.QuickFix); - c.setTitle("Upgrade to Spring Boot " + latest.toString() + " (Maven dependency version changes only)"); - String commandId = SpringBootUpgrade.CMD_UPGRADE_SPRING_BOOT; - c.setCommand(new Command("Upgrade to Version " + latest.toString(), commandId, - ImmutableList.of(javaProject.getLocationUri().toASCIIString(), latest.toString(), false))); - return c; - }).ifPresent(actions::add); - } - - actions.add(openReleaseNotesCodeAction(latest)); - - return Optional.ofNullable(createDiagnostic(actions, problemType, message.toString())); + private Optional validatePatchVersion(IJavaProject javaProject, Version javaProjectVersion, Version latest) { + List actions = new ArrayList<>(2); + if (canProvideQuickfix(javaProject)) { + bootUpgradeOpt.map(bu -> { + CodeAction c = new CodeAction(); + c.setKind(CodeActionKind.QuickFix); + c.setTitle("Upgrade to Spring Boot " + latest.toString() + " (Maven dependency version changes only)"); + String commandId = SpringBootUpgrade.CMD_UPGRADE_SPRING_BOOT; + c.setCommand(new Command("Upgrade to Version " + latest.toString(), commandId, + ImmutableList.of(javaProject.getLocationUri().toASCIIString(), latest.toString(), false))); + return c; + }).ifPresent(actions::add); } - return Optional.empty(); + + actions.add(openReleaseNotesCodeAction(latest)); + + return Optional.ofNullable(createDiagnostic(actions, VersionValidationProblemType.UPDATE_LATEST_PATCH_VERSION, "Newer patch version of Spring Boot available: %s".formatted(latest.toString()))); } private static CodeAction openReleaseNotesCodeAction(Version version) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/VersionValidationUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/VersionValidationUtils.java index 2251c2fe15..213c0b3272 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/VersionValidationUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/VersionValidationUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022, 2023 VMware, Inc. + * Copyright (c) 2022, 2026 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -43,61 +43,4 @@ public static Version getLatestSupportedRelease(ResolvedSpringProject springProj return rls.isEmpty() ? null : rls.get(rls.size() - 1); } - public static Version getNewestPatchRelease(List releases, Version version) { - Version result = version; - - for (Version release : releases) { - if (release.getMajor() == result.getMajor() - && release.getMinor() == result.getMinor() - && release.getPatch() > result.getPatch()) { - result = release; - } - } - - return result; - } - - public static Version getNewestMinorRelease(List releases, Version version) { - Version result = version; - - for (Version release : releases) { - if (release.getMajor() == result.getMajor() - && (release.getMinor() > result.getMinor() - || (release.getMinor() == result.getMinor() && release.getPatch() > result.getPatch()))) { - result = release; - } - } - - return result; - } - - public static Version getNewestMajorRelease(List releases, Version version) { - Version result = version; - - for (Version release : releases) { - if (release.getMajor() > result.getMajor() - || (release.getMajor() == result.getMajor() && release.getMinor() > result.getMinor()) - || (release.getMajor() == result.getMajor() && release.getMinor() == result.getMinor() && release.getPatch() > result.getPatch())) { - result = release; - } - } - - return result; - } - - public static Version getNewerLatestPatchRelease(List releases, Version version) { - Version newestPatch = getNewestPatchRelease(releases, version); - return newestPatch.equals(version) ? null : newestPatch; - } - - public static Version getNewerLatestMinorRelease(List releases, Version version) { - Version newestMinor = getNewestMinorRelease(releases, version); - return newestMinor.getMinor() == version.getMinor() ? null : newestMinor; - } - - public static Version getNewerLatestMajorRelease(List releases, Version version) { - Version newestMajor = getNewestMajorRelease(releases, version); - return newestMajor.getMajor() == version.getMajor() ? null : newestMajor; - } - } 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 660a738784..ccbdd7fbf7 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.eclipse.lsp4j.Position; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.boot.validation.generations.MavenMetadataProvider; 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; @@ -80,7 +81,8 @@ void inlayProvided() throws Exception { SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); when(projectProvider.getProject(SpringProjectUtil.SPRING_BOOT)).thenReturn(resolvedProject); - PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + MavenMetadataProvider mavenMetadataProvider = mock(MavenMetadataProvider.class); + PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider, mavenMetadataProvider); List hints = inlayHandler.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); @@ -125,7 +127,8 @@ void inlayNotProvidedOutOfOssSupport() throws Exception { SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); when(projectProvider.getProject(SpringProjectUtil.SPRING_BOOT)).thenReturn(resolvedProject); - PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + MavenMetadataProvider mavenMetadataProvider = mock(MavenMetadataProvider.class); + PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider, mavenMetadataProvider); List hints = inlayHandler.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(0, hints.size()); @@ -161,7 +164,8 @@ void upgradePatchVersionInlay() throws Exception { SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); when(projectProvider.getProject(SpringProjectUtil.SPRING_BOOT)).thenReturn(resolvedProject); - PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + MavenMetadataProvider mavenMetadataProvider = mock(MavenMetadataProvider.class); + PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider, mavenMetadataProvider); List hints = inlayHandler.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(1, hints.size()); @@ -217,7 +221,8 @@ void noInlayHintOnEmptyVersionTag() throws Exception { SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); when(projectProvider.getProject(SpringProjectUtil.SPRING_BOOT)).thenReturn(resolvedProject); - PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + MavenMetadataProvider mavenMetadataProvider = mock(MavenMetadataProvider.class); + PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider, mavenMetadataProvider); List hints = inlayHandler.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(0, hints.size()); @@ -258,7 +263,8 @@ void noInlayHintOnEmptyVersionWithSpacesTag() throws Exception { SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); when(projectProvider.getProject(SpringProjectUtil.SPRING_BOOT)).thenReturn(resolvedProject); - PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + MavenMetadataProvider mavenMetadataProvider = mock(MavenMetadataProvider.class); + PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider, mavenMetadataProvider); List hints = inlayHandler.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(0, hints.size()); @@ -299,7 +305,8 @@ void noInlayHintOnVersionTagWithNonParseableValue() throws Exception { SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); when(projectProvider.getProject(SpringProjectUtil.SPRING_BOOT)).thenReturn(resolvedProject); - PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + MavenMetadataProvider mavenMetadataProvider = mock(MavenMetadataProvider.class); + PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider, mavenMetadataProvider); List hints = inlayHandler.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(0, hints.size()); @@ -331,7 +338,8 @@ void noInlayHintWhenBuildFileUriIsNull() throws Exception { SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); - PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + MavenMetadataProvider mavenMetadataProvider = mock(MavenMetadataProvider.class); + PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider, mavenMetadataProvider); List hints = inlayHandler.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(0, hints.size()); @@ -359,7 +367,8 @@ void noInlayHintWhenNotSpringBootProject() throws Exception { SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); - PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + MavenMetadataProvider mavenMetadataProvider = mock(MavenMetadataProvider.class); + PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider, mavenMetadataProvider); List hints = inlayHandler.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(0, hints.size()); @@ -393,7 +402,8 @@ void upgradePatchVersionInlay_AlreadyOnLatestPatch() throws Exception { SpringProjectsProvider projectProvider = mock(SpringProjectsProvider.class); when(projectProvider.getProject(SpringProjectUtil.SPRING_BOOT)).thenReturn(resolvedProject); - PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); + MavenMetadataProvider mavenMetadataProvider = mock(MavenMetadataProvider.class); + PomInlayHintHandler inlayHandler = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider, mavenMetadataProvider); List hints = inlayHandler.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(0, hints.size()); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/generations/SortedVersionsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/generations/SortedVersionsTest.java new file mode 100644 index 0000000000..5edbdce368 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/generations/SortedVersionsTest.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2022, 2026 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.validation.generations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.commons.Version; + +public class SortedVersionsTest { + + final List releases = Arrays.asList( + new Version(1,5,3,"RELEASE"), + new Version(1,5,10,"RELEASE"), + new Version(1,5,12,"RELEASE"), + new Version(2,0,0,null), + new Version(2,0,3,null), + new Version(2,1,6,null), + new Version(2,4,15,null), + new Version(2,5,21,null), + new Version(2,6,10,null), + new Version(2,6,18,null), + new Version(2,7,3,null), + new Version(2,7,6,null), + new Version(3,0,0,null) + ); + + final SortedVersions sortedVersions = new SortedVersions(releases); + + @Test + void testGetNewerLatestPatchRelease() { + assertEquals(Optional.of(new Version(1,5,12,"RELEASE")), sortedVersions.getNewerLatestPatchRelease(new Version(1,5,3, "RELEASE"))); + assertFalse(sortedVersions.getNewerLatestPatchRelease(new Version(1,4,3, null)).isPresent()); + assertEquals(Optional.of(new Version(2,1,6,null)), sortedVersions.getNewerLatestPatchRelease(new Version(2,1,3, null))); + assertEquals(Optional.of(new Version(2,6,18,null)), sortedVersions.getNewerLatestPatchRelease(new Version(2,6,5, null))); + assertFalse(sortedVersions.getNewerLatestPatchRelease(new Version(2,6,18, null)).isPresent()); + assertFalse(sortedVersions.getNewerLatestPatchRelease(new Version(3,0,0, null)).isPresent()); + assertFalse(sortedVersions.getNewerLatestPatchRelease(new Version(3,0,1, null)).isPresent()); + } + + @Test + void testGetNewerLatestMinorRelease() { + assertFalse(sortedVersions.getNewerLatestMinorRelease(new Version(1,5,3, "RELEASE")).isPresent()); + assertEquals(Optional.of(new Version(1,5,12,"RELEASE")), sortedVersions.getNewerLatestMinorRelease(new Version(1,4,3, "RELEASE"))); + assertEquals(Optional.of(new Version(2,7,6,null)), sortedVersions.getNewerLatestMinorRelease(new Version(2,1,3, null))); + assertEquals(Optional.of(new Version(2,7,6,null)), sortedVersions.getNewerLatestMinorRelease(new Version(2,6,5, null))); + assertFalse(sortedVersions.getNewerLatestMinorRelease(new Version(2,7,3, null)).isPresent()); + assertFalse(sortedVersions.getNewerLatestMinorRelease(new Version(2,7,6, null)).isPresent()); + assertFalse(sortedVersions.getNewerLatestMinorRelease(new Version(3,0,0, null)).isPresent()); + assertFalse(sortedVersions.getNewerLatestMinorRelease(new Version(3,0,1, null)).isPresent()); + } + + @Test + void testGetNewerLatestMajorRelease() { + assertEquals(Optional.of(new Version(3,0,0,null)), sortedVersions.getNewerLatestMajorRelease(new Version(1,5,3, "RELEASE"))); + assertEquals(Optional.of(new Version(3,0,0,null)), sortedVersions.getNewerLatestMajorRelease(new Version(1,4,3, null))); + assertEquals(Optional.of(new Version(3,0,0,null)), sortedVersions.getNewerLatestMajorRelease(new Version(2,1,3, null))); + assertEquals(Optional.of(new Version(3,0,0,null)), sortedVersions.getNewerLatestMajorRelease(new Version(2,6,5, null))); + assertFalse(sortedVersions.getNewerLatestMajorRelease(new Version(3,0,0, null)).isPresent()); + assertFalse(sortedVersions.getNewerLatestMajorRelease(new Version(3,0,1, null)).isPresent()); + assertFalse(sortedVersions.getNewerLatestMajorRelease(new Version(4,1,3, null)).isPresent()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/test/VersionValidationUtilsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/test/VersionValidationUtilsTest.java deleted file mode 100644 index bf3a2d3d6d..0000000000 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/test/VersionValidationUtilsTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2022 VMware, Inc. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * VMware, Inc. - initial API and implementation - *******************************************************************************/ -package org.springframework.ide.vscode.boot.validation.test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.util.Arrays; -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.springframework.ide.vscode.boot.validation.generations.VersionValidationUtils; -import org.springframework.ide.vscode.commons.Version; - -public class VersionValidationUtilsTest { - - final List releases = Arrays.asList( - new Version(1,5,3,"RELEASE"), - new Version(1,5,10,"RELEASE"), - new Version(1,5,12,"RELEASE"), - new Version(2,0,0,null), - new Version(2,0,3,null), - new Version(2,1,6,null), - new Version(2,4,15,null), - new Version(2,5,21,null), - new Version(2,6,10,null), - new Version(2,6,18,null), - new Version(2,7,3,null), - new Version(2,7,6,null), - new Version(3,0,0,null) - ); - - @Test - void testFindNewestPatchRelease() { - assertEquals(new Version(1,5,12,"RELEASE"), VersionValidationUtils.getNewestPatchRelease(releases, new Version(1,5,3, "RELEASE"))); - assertEquals(new Version(1,4,3, null), VersionValidationUtils.getNewestPatchRelease(releases, new Version(1,4,3, null))); - assertEquals(new Version(2,1,6,null), VersionValidationUtils.getNewestPatchRelease(releases, new Version(2,1,3, null))); - assertEquals(new Version(2,6,18,null), VersionValidationUtils.getNewestPatchRelease(releases, new Version(2,6,5, null))); - assertEquals(new Version(3,0,0, null), VersionValidationUtils.getNewestPatchRelease(releases, new Version(3,0,0, null))); - assertEquals(new Version(3,0,1, null), VersionValidationUtils.getNewestPatchRelease(releases, new Version(3,0,1, null))); - } - - @Test - void testGetNewerLatestPatchRelease() { - assertEquals(new Version(1,5,12,"RELEASE"), VersionValidationUtils.getNewerLatestPatchRelease(releases, new Version(1,5,3, "RELEASE"))); - assertNull(VersionValidationUtils.getNewerLatestPatchRelease(releases, new Version(1,4,3, null))); - assertEquals(new Version(2,1,6,null), VersionValidationUtils.getNewerLatestPatchRelease(releases, new Version(2,1,3, null))); - assertEquals(new Version(2,6,18,null), VersionValidationUtils.getNewerLatestPatchRelease(releases, new Version(2,6,5, null))); - assertNull(VersionValidationUtils.getNewerLatestPatchRelease(releases, new Version(2,6,18, null))); - assertNull(VersionValidationUtils.getNewerLatestPatchRelease(releases, new Version(3,0,0, null))); - assertNull(VersionValidationUtils.getNewerLatestPatchRelease(releases, new Version(3,0,1, null))); - } - - @Test - void testFindNewestMinorRelease() { - assertEquals(new Version(1,5,12,"RELEASE"), VersionValidationUtils.getNewestMinorRelease(releases, new Version(1,5,3, "RELEASE"))); - assertEquals(new Version(1,5,12,"RELEASE"), VersionValidationUtils.getNewestMinorRelease(releases, new Version(1,4,3, "RELEASE"))); - assertEquals(new Version(2,7,6,null), VersionValidationUtils.getNewestMinorRelease(releases, new Version(2,1,3, null))); - assertEquals(new Version(2,7,6,null), VersionValidationUtils.getNewestMinorRelease(releases, new Version(2,6,5, null))); - assertEquals(new Version(2,7,6, null), VersionValidationUtils.getNewestMinorRelease(releases, new Version(2,7,3, null))); - assertEquals(new Version(2,7,6, null), VersionValidationUtils.getNewestMinorRelease(releases, new Version(2,7,6, null))); - assertEquals(new Version(3,0,0, null), VersionValidationUtils.getNewestMinorRelease(releases, new Version(3,0,0, null))); - assertEquals(new Version(3,0,1, null), VersionValidationUtils.getNewestMinorRelease(releases, new Version(3,0,1, null))); - } - - @Test - void testGetNewerLatestMinorRelease() { - assertNull(VersionValidationUtils.getNewerLatestMinorRelease(releases, new Version(1,5,3, "RELEASE"))); - assertEquals(new Version(1,5,12,"RELEASE"), VersionValidationUtils.getNewerLatestMinorRelease(releases, new Version(1,4,3, "RELEASE"))); - assertEquals(new Version(2,7,6,null), VersionValidationUtils.getNewerLatestMinorRelease(releases, new Version(2,1,3, null))); - assertEquals(new Version(2,7,6,null), VersionValidationUtils.getNewerLatestMinorRelease(releases, new Version(2,6,5, null))); - assertNull(VersionValidationUtils.getNewerLatestMinorRelease(releases, new Version(2,7,3, null))); - assertNull(VersionValidationUtils.getNewerLatestMinorRelease(releases, new Version(2,7,6, null))); - assertNull(VersionValidationUtils.getNewerLatestMinorRelease(releases, new Version(3,0,0, null))); - assertNull(VersionValidationUtils.getNewerLatestMinorRelease(releases, new Version(3,0,1, null))); - } - - @Test - void testFindNewestMajorRelease() { - assertEquals(new Version(3,0,0,null), VersionValidationUtils.getNewestMajorRelease(releases, new Version(1,5,3, "RELEASE"))); - assertEquals(new Version(3,0,0,null), VersionValidationUtils.getNewestMajorRelease(releases, new Version(1,4,3, null))); - assertEquals(new Version(3,0,0,null), VersionValidationUtils.getNewestMajorRelease(releases, new Version(2,1,3, null))); - assertEquals(new Version(3,0,0,null), VersionValidationUtils.getNewestMajorRelease(releases, new Version(2,6,5, null))); - assertEquals(new Version(3,0,0, null), VersionValidationUtils.getNewestMajorRelease(releases, new Version(3,0,0, null))); - assertEquals(new Version(3,0,1, null), VersionValidationUtils.getNewestMajorRelease(releases, new Version(3,0,1, null))); - assertEquals(new Version(4,1,3, null), VersionValidationUtils.getNewestMajorRelease(releases, new Version(4,1,3, null))); - } - - @Test - void testGetNewerLatestMajorRelease() { - assertEquals(new Version(3,0,0,null), VersionValidationUtils.getNewerLatestMajorRelease(releases, new Version(1,5,3, "RELEASE"))); - assertEquals(new Version(3,0,0,null), VersionValidationUtils.getNewerLatestMajorRelease(releases, new Version(1,4,3, null))); - assertEquals(new Version(3,0,0,null), VersionValidationUtils.getNewerLatestMajorRelease(releases, new Version(2,1,3, null))); - assertEquals(new Version(3,0,0,null), VersionValidationUtils.getNewerLatestMajorRelease(releases, new Version(2,6,5, null))); - assertNull(VersionValidationUtils.getNewerLatestMajorRelease(releases, new Version(3,0,0, null))); - assertNull(VersionValidationUtils.getNewerLatestMajorRelease(releases, new Version(3,0,1, null))); - assertNull(VersionValidationUtils.getNewerLatestMajorRelease(releases, new Version(4,1,3, null))); - } - -}