Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Supplier;
Expand Down Expand Up @@ -135,11 +136,13 @@ public List<InlayHint> handle(TextDocument doc, Range range, CancelChecker cance

try {
SortedVersions versions = null;
boolean usedMavenMetadata = false;
if (bootJavaConfig.isUseProjectBuildFileForVersionValidation() && 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();
usedMavenMetadata = true;
}
} catch (Exception e) {
// Logged in provider, fallback will happen below
Expand All @@ -148,19 +151,42 @@ public List<InlayHint> handle(TextDocument doc, Range range, CancelChecker cance

ResolvedSpringProject genProject = generationsProvider.getProject(SpringProjectUtil.SPRING_BOOT);
if (versions == null && genProject != null) {
versions = new SortedVersions(genProject.getReleases());
versions = new SortedVersions(genProject.getLatestPatchVersions());
}

if (versions != null) {
Version latestPatch = versions.getNewerLatestPatchRelease(currentVersion).orElse(null);
if (latestPatch != null) {
// type is "oss"/"enterprise", or "" for the generic single-candidate case.
Optional<Generation> currentGeneration = usedMavenMetadata || genProject == null
? Optional.empty()
: genProject.findGeneration(currentVersion);

Map<String, Version> patchCandidatesByType;
if (currentGeneration.isPresent()) {
patchCandidatesByType = currentGeneration.get().getLatestPatchByType();
} else {
patchCandidatesByType = versions.getNewerLatestPatchRelease(currentVersion)
.map(latest -> Map.of("", latest))
.orElse(Map.of());
}

List<Map.Entry<String, Version>> patchTargets = patchCandidatesByType.entrySet().stream()
.filter(e -> e.getValue().compareTo(currentVersion) > 0)
.sorted(Map.Entry.<String, Version>comparingByValue().reversed())
.collect(Collectors.toList());

for (Map.Entry<String, Version> patchTarget : patchTargets) {
String type = patchTarget.getKey();
Version latestPatch = patchTarget.getValue();
String title = type.isEmpty() ? "Upgrade to the Latest Patch"
: "Upgrade to the Latest Patch (" + VersionValidationUtils.patchTypeLabel(type) + ")";

inlayHintProviders.add(new InlayHintWithLazyPosition(() -> {
Command command = new Command();
command.setTitle("Upgrade to the Latest Patch");
command.setTitle(title);
command.setCommand(SpringBootPatchUpgrade.CMD_UPGRADE_SPRING_BOOT_PATCH);
command.setArguments(List.of(jp.getLocationUri().toASCIIString(), latestPatch.toString(), false));

InlayHintLabelPart label = new InlayHintLabelPart("Upgrade to the Latest Patch");
InlayHintLabelPart label = new InlayHintLabelPart(title);
label.setCommand(command);

InlayHint hint = new InlayHint();
Expand Down Expand Up @@ -195,7 +221,7 @@ public List<InlayHint> handle(TextDocument doc, Range range, CancelChecker cance
return Collections.emptyList();
}));
}

if (genProject != null) {
Generation generation = GenerationsValidator.getGenerationForJavaProject(jp, genProject, genProject.getSlug());
if (generation != null && VersionValidationUtils.isOssValid(generation)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionKind;
Expand All @@ -26,6 +28,8 @@
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.app.BootLanguageServerInitializer;
import org.springframework.ide.vscode.boot.java.rewrite.SpringBootPatchUpgrade;
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.boot.validation.generations.preferences.VersionValidationProblemType;
import org.springframework.ide.vscode.commons.Version;
import org.springframework.ide.vscode.commons.java.IJavaProject;
Expand Down Expand Up @@ -60,7 +64,8 @@ public Collection<Diagnostic> validate(IJavaProject javaProject, Version javaPro
long start = System.currentTimeMillis();
try {
SortedVersions versions = null;

Map<String, Version> patchCandidatesByType;

if (bootJavaConfig.isUseProjectBuildFileForVersionValidation() && ProjectBuild.MAVEN_PROJECT_TYPE.equals(javaProject.getProjectBuild().getType())) {
try {
MavenMetadata metadata = mavenMetadataProvider.getMetadata(javaProject, "org.springframework.boot", "spring-boot");
Expand All @@ -71,26 +76,35 @@ public Collection<Diagnostic> validate(IJavaProject javaProject, Version javaPro
// Logged in provider, fallback will happen below
}
}


Optional<Generation> currentGeneration = Optional.empty();
if (versions == null) {
List<Version> rawReleases = springProjectsProvider.getProject(SpringProjectUtil.SPRING_BOOT).getReleases();
versions = new SortedVersions(rawReleases);
ResolvedSpringProject bootProject = springProjectsProvider.getProject(SpringProjectUtil.SPRING_BOOT);
versions = new SortedVersions(bootProject.getLatestPatchVersions());
currentGeneration = bootProject.findGeneration(javaProjectVersion);
}


if (currentGeneration.isPresent()) {
patchCandidatesByType = currentGeneration.get().getLatestPatchByType();
} else {
patchCandidatesByType = versions.getNewerLatestPatchRelease(javaProjectVersion)
.map(latest -> Map.of("", latest))
.orElse(Map.of());
}

ImmutableList.Builder<Diagnostic> builder = ImmutableList.builder();

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))

validatePatchVersions(javaProject, javaProjectVersion, patchCandidatesByType)
.ifPresent(builder::add);

return builder.build();
} finally {
log.info("boot major/minor/patch version validation for `%s` took: %d".formatted(javaProject.getElementName(), System.currentTimeMillis() - start));
Expand All @@ -117,25 +131,53 @@ private Optional<Diagnostic> validateMinorVersion(IJavaProject javaProject, Vers
return Optional.ofNullable(createDiagnostic(actions, VersionValidationProblemType.UPDATE_LATEST_MINOR_VERSION, "Newer minor version of Spring Boot available: %s".formatted(latest.toString())));
}

private Optional<Diagnostic> validatePatchVersion(IJavaProject javaProject, Version javaProjectVersion, Version latest) {
List<CodeAction> 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 = SpringBootPatchUpgrade.CMD_UPGRADE_SPRING_BOOT_PATCH;
c.setCommand(new Command("Upgrade to Version " + latest.toString(), commandId,
ImmutableList.of(javaProject.getLocationUri().toASCIIString(), latest.toString(), false)));
return c;
}).ifPresent(actions::add);
/**
* One quickfix per candidate newer than the current version (e.g. oss + enterprise).
*/
private Optional<Diagnostic> validatePatchVersions(IJavaProject javaProject, Version javaProjectVersion, Map<String, Version> candidatesByType) {
List<Map.Entry<String, Version>> newerCandidates = candidatesByType.entrySet().stream()
.filter(e -> e.getValue().compareTo(javaProjectVersion) > 0)
.sorted(Map.Entry.<String, Version>comparingByValue().reversed())
.collect(Collectors.toList());

if (newerCandidates.isEmpty()) {
return Optional.empty();
}

actions.add(openReleaseNotesCodeAction(latest));
List<CodeAction> actions = new ArrayList<>();
for (Map.Entry<String, Version> candidate : newerCandidates) {
String type = candidate.getKey();
Version latest = candidate.getValue();
String qualifier = type.isEmpty() ? "Maven dependency version changes only"
: VersionValidationUtils.patchTypeLabel(type) + ", Maven dependency version changes only";

if (canProvideQuickfix(javaProject)) {
bootUpgradeOpt.map(bu -> {
CodeAction c = new CodeAction();
c.setKind(CodeActionKind.QuickFix);
c.setTitle("Upgrade to Spring Boot " + latest.toString() + " (" + qualifier + ")");
String commandId = SpringBootPatchUpgrade.CMD_UPGRADE_SPRING_BOOT_PATCH;
c.setCommand(new Command("Upgrade to Version " + latest.toString(), commandId,
ImmutableList.of(javaProject.getLocationUri().toASCIIString(), latest.toString(), false)));
return c;
}).ifPresent(actions::add);
}

// Release notes for commercial-only patches aren't publicly published, so skip those.
if (!"enterprise".equals(type)) {
actions.add(openReleaseNotesCodeAction(latest));
}
}

String message = newerCandidates.size() == 1 && newerCandidates.get(0).getKey().isEmpty()
? "Newer patch version of Spring Boot available: %s".formatted(newerCandidates.get(0).getValue())
: newerCandidates.stream()
.map(e -> "%s (%s)".formatted(e.getValue(), VersionValidationUtils.patchTypeLabel(e.getKey())))
.collect(Collectors.joining(", ", "Newer patch version of Spring Boot available: ", ""));

return Optional.ofNullable(createDiagnostic(actions, VersionValidationProblemType.UPDATE_LATEST_PATCH_VERSION, "Newer patch version of Spring Boot available: %s".formatted(latest.toString())));
return Optional.ofNullable(createDiagnostic(actions, VersionValidationProblemType.UPDATE_LATEST_PATCH_VERSION, message));
}

private static CodeAction openReleaseNotesCodeAction(Version version) {
CodeAction releaseNoteLink = new CodeAction();
releaseNoteLink.setKind(CodeActionKind.QuickFix);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@
package org.springframework.ide.vscode.boot.validation.generations;

import java.sql.Date;
import java.util.List;

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;

public class VersionValidationUtils {

Expand All @@ -37,10 +34,16 @@ public static boolean isCommercialValid(Generation gen) {
return false;
}

public static Version getLatestSupportedRelease(ResolvedSpringProject springProject)
throws Exception {
List<Version> rls = springProject.getReleases();
return rls.isEmpty() ? null : rls.get(rls.size() - 1);
/**
* Human-readable label for a {@code Generation.latestPatch} entry key
* ({@code oss} or {@code enterprise}).
*/
public static String patchTypeLabel(String type) {
return switch (type) {
case "oss" -> "OSS";
case "enterprise" -> "Enterprise";
default -> type;
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.validation.generations.json;

import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.ide.vscode.commons.Version;

public class Generation extends JsonHalLinks {

private String name;
private String ossSupportEndDate;
private String commercialSupportEndDate;
private String initialReleaseDate;
private Map<String, String[]> linkedGenerations;
private Map<String, String> latestPatch;

public String getName() {
return name;
Expand Down Expand Up @@ -60,4 +64,38 @@ public void setLinkedGenerations(Map<String, String[]> linkedGenerations) {
this.linkedGenerations = linkedGenerations;
}

public Map<String, String> getLatestPatch() {
return latestPatch;
}

public void setLatestPatch(Map<String, String> latestPatch) {
this.latestPatch = latestPatch;
}

/**
* OSS-only latest patch; {@code null} once OSS support has ended (enterprise-only from then on).
*/
public Version getLatestPatchVersion() {
if (latestPatch == null) {
return null;
}
return Version.parse(latestPatch.get("oss"));
}

/**
* All {@code latestPatch} entries, keyed by type ({@code oss}/{@code enterprise}).
*/
public Map<String, Version> getLatestPatchByType() {
Map<String, Version> result = new LinkedHashMap<>();
if (latestPatch != null) {
for (Map.Entry<String, String> e : latestPatch.entrySet()) {
Version v = Version.parse(e.getValue());
if (v != null) {
result.put(e.getKey(), v);
}
}
}
return result;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022, 2025 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
Expand All @@ -11,6 +11,8 @@
package org.springframework.ide.vscode.boot.validation.generations.json;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import org.springframework.ide.vscode.boot.validation.generations.SpringProjectsClient;
Expand Down Expand Up @@ -49,6 +51,27 @@ public List<Generation> getGenerations() throws Exception {
return this.generations != null ? this.generations.getGenerations() : ImmutableList.of();
}

/**
* OSS latest patch per generation, including generations no longer in support.
*/
public List<Version> getLatestPatchVersions() throws Exception {
return getGenerations().stream()
.map(Generation::getLatestPatchVersion)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}

/**
* The generation whose name matches {@code <major>.<minor>.x} for the given version,
* e.g. {@code 3.4.7} -> generation {@code 3.4.x}.
*/
public Optional<Generation> findGeneration(Version version) throws Exception {
String generationName = version.getMajor() + "." + version.getMinor() + ".x";
return getGenerations().stream()
.filter(g -> generationName.equals(g.getName()))
.findFirst();
}

/**
* Sorted list of released versions
*/
Expand Down
Loading
Loading