diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/SpringProjectUtil.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/SpringProjectUtil.java index 72cd682185..ffef3a131c 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/SpringProjectUtil.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/SpringProjectUtil.java @@ -32,13 +32,13 @@ public class SpringProjectUtil { public static final String SPRING_BOOT = "spring-boot"; public static final String SPRING_WEB = "spring-web"; - + private static final String GENERATION_VERSION_STR = "([0-9]+)"; public static final Logger log = LoggerFactory.getLogger(SpringProjectUtil.class); - + private static final Pattern GENERATION_VERSION = Pattern.compile(GENERATION_VERSION_STR); - + public static boolean isSpringProject(IJavaProject jp) { return jp.getClasspath().findBinaryLibraryByPrefix("spring-core").isPresent(); } @@ -50,50 +50,43 @@ public static boolean isBootProject(IJavaProject jp) { public static boolean hasBootActuators(IJavaProject jp) { return jp.getClasspath().findBinaryLibraryByPrefix("spring-boot-actuator-").isPresent(); } - + /** - * Parses version from the given generation name (e.g. "2.1.x" - * @param name - * @return Version if valid generation name with major and minor components - * @throws Exception if invalid generation name + * Parses a {@link Version} from the given generation name (e.g. {@code "2.1.x"} → {@code 2.1.0}). + * + * @param name generation name containing at least major and minor numeric components + * @return parsed {@link Version} + * @throws IllegalArgumentException if the name does not contain major and minor components */ public static Version getVersionFromGeneration(String name) throws Exception { Matcher matcher = GENERATION_VERSION.matcher(name); String major = null; String minor = null; - + if (matcher.find()) { - int start = matcher.start(); - int end = matcher.end(); - major = name.substring(start, end); + major = name.substring(matcher.start(), matcher.end()); } - if (matcher.find()) { - int start = matcher.start(); - int end = matcher.end(); - minor = name.substring(start, end); + minor = name.substring(matcher.start(), matcher.end()); } - + if (major != null && minor != null) { - return new Version( - Integer.parseInt(major), - Integer.parseInt(minor), - 0, - null - ); + return Version.parse("%s.%s.0".formatted(major, minor)); } throw new IllegalArgumentException("Invalid semver. Unable to parse major and minor version from: " + name); } public static Version getDependencyVersionByPrefix(IJavaProject jp, String dependencyPrefix) { - return jp.getClasspath().findBinaryLibraryByPrefix(dependencyPrefix).map(cpe -> cpe.getVersion()).orElse(null); + return jp.getClasspath().findBinaryLibraryByPrefix(dependencyPrefix) + .map(cpe -> Version.parse(cpe.getVersion())).orElse(null); } - - public static Version getDependencyVersionByName(IJavaProject jp, String name) { - return jp.getClasspath().findBinaryLibraryByName(name).map(cpe -> cpe.getVersion()).orElse(null); + + public static Version getDependencyVersionByName(IJavaProject jp, String name) { + return jp.getClasspath().findBinaryLibraryByName(name) + .map(cpe -> Version.parse(cpe.getVersion())).orElse(null); } - + public static boolean hasDependencyStartingWith(IJavaProject jp, String dependency, Predicate filter) { IClasspath classpath = jp.getClasspath(); return classpath.findBinaryLibraryByPrefix(dependency).or(() -> { @@ -123,7 +116,7 @@ public static boolean hasDependencyStartingWith(IJavaProject jp, String dependen public static Version getSpringBootVersion(IJavaProject jp) { return getDependencyVersionByName(jp, SPRING_BOOT); } - + public static Predicate springBootVersionGreaterOrEqual(int major, int minor, int patch) { return libraryVersionGreaterOrEqual(SPRING_BOOT, major, minor, patch); } @@ -131,29 +124,15 @@ public static Predicate springBootVersionGreaterOrEqual(int major, public static Predicate libraryVersionGreaterOrEqual(String libraryName, int major, int minor, int patch) { return project -> { Version version = getDependencyVersionByName(project, libraryName); - if (version == null) { - return false; - } - if (major > version.getMajor()) { - return false; - } - if (major == version.getMajor()) { - if (minor > version.getMinor()) { - return false; - } - if (minor == version.getMinor()) { - return patch <= version.getPatch(); - } - } - return true; + return version != null && version.compareTo(Version.parse("%d.%d.%d".formatted(major, minor, patch))) >= 0; }; } - + /** * Finds Spring Boot application.properties and application.yml files in the project's resource folders. * Prioritizes main application.properties/yml files over profile-specific files (application-*.properties/yml). * Excludes test resources. - * + * * @param project the Java project * @return list of Boot properties/YAML file paths, with main files prioritized over profile-specific files */ @@ -177,8 +156,6 @@ public static List findBootPropertiesFiles(IJavaProject project) { // ignore return Collections.emptyList(); } - } - } 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 de701d0a44..a111c0501a 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 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 @@ -14,20 +14,86 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * Represents a parsed artifact version with support for multi-part numeric versions + * (major.minor.patch.build) and pre-release qualifiers (M, RC, SNAPSHOT, etc.). + *

+ * Sorting follows the same qualifier precedence as OpenRewrite's {@code LatestRelease}: + * alpha < beta < milestone < rc < snapshot < release < service-pack. + *

+ * The comparison and parsing logic in this class is derived from + * {@code org.openrewrite.semver.VersionComparator} and {@code org.openrewrite.semver.LatestRelease} + * (Apache License 2.0, Copyright 2021 the OpenRewrite authors). + * + * @author Alex Boyko + */ public final class Version implements Comparable { - - private static final Pattern VERSION_PATTERN = Pattern.compile("(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:(-|\\.)((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); - - private int major; - private int minor; - private int patch; - private String qualifier; - - public Version(int major, int minor, int patch, String qualifier) { + + // Derived from org.openrewrite.semver.VersionComparator (Apache 2.0) + // Groups: 1=major, 2=minor, 3=patch, 4=build (4th numeric part), 5=fifth part, 6=qualifier suffix + private static final Pattern RELEASE_PATTERN = Pattern.compile( + "(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+))?([-.+].*)?"); + private static final int QUALIFIER_GROUP = 6; + + private static final Pattern QUALIFIER_TYPE_PATTERN = + Pattern.compile("^(snapshot|alpha|a|beta|b|milestone|m|rc|cr|sp)(\\d*)$", + Pattern.CASE_INSENSITIVE); + + /** + * The release category of a version, ordered from least to most stable. + */ + public enum ReleaseType { + ALPHA(1), + BETA(2), + MILESTONE(3), + RC(4), + SNAPSHOT(5), + RELEASE(6), + SERVICE_PACK(7); + + private final int priority; + + ReleaseType(int priority) { + this.priority = priority; + } + + /** Numeric ordering value — higher means more stable/newer. */ + public int getPriority() { + return priority; + } + + /** True for everything before a GA release (alpha, beta, milestone, rc, snapshot). */ + public boolean isPreRelease() { + return priority < RELEASE.priority; + } + + public boolean isSnapshot() { + return this == SNAPSHOT; + } + } + + private final int major; + private final int minor; + private final int patch; + /** Fourth numeric component (e.g. the {@code 1} in {@code 3.3.0.1}); 0 if absent. */ + private final int build; + /** Qualifier text without its leading separator (e.g. {@code "M1"}, {@code "SNAPSHOT"}), or {@code null}. */ + private final String qualifier; + private final ReleaseType releaseType; + /** Numeric suffix of the qualifier (e.g. {@code 1} for {@code M1} or {@code RC2→2}); 0 if absent. */ + private final int qualifierNumber; + /** Original parsed string — used for display. */ + private final String versionString; + + private Version(int major, int minor, int patch, int build, String qualifier, String originalString) { this.major = major; this.minor = minor; this.patch = patch; + this.build = build; this.qualifier = qualifier; + this.releaseType = toReleaseType(qualifier); + this.qualifierNumber = toQualifierNumber(qualifier); + this.versionString = originalString; } public int getMajor() { @@ -42,107 +108,181 @@ public int getPatch() { return patch; } + /** Fourth numeric version component; 0 when not present (e.g. {@code 3.3.0} → 0). */ + public int getBuild() { + return build; + } + + /** + * Raw qualifier text without its leading separator, or {@code null} for GA releases. + * Examples: {@code "SNAPSHOT"}, {@code "M1"}, {@code "RC2"}. + */ public String getQualifier() { return qualifier; } - + + public ReleaseType getReleaseType() { + return releaseType; + } + + /** Numeric suffix of the qualifier; 0 when absent (e.g. {@code SNAPSHOT→0}, {@code M1→1}, {@code RC2→2}). */ + public int getQualifierNumber() { + return qualifierNumber; + } + + /** True for GA releases and service packs. */ + public boolean isRelease() { + return releaseType == ReleaseType.RELEASE || releaseType == ReleaseType.SERVICE_PACK; + } + + /** True for any version that is not yet a GA release (alpha, beta, M, RC, SNAPSHOT). */ + public boolean isPreRelease() { + return releaseType.isPreRelease(); + } + + public boolean isSnapshot() { + return releaseType == ReleaseType.SNAPSHOT; + } + public String toMajorMinorVersionStr() { - StringBuilder sb = new StringBuilder(); - sb.append(major); - sb.append('.'); - sb.append(minor); - return sb.toString(); + return major + "." + minor; } public String toMajorMinorPatchVersionStr() { - StringBuilder sb = new StringBuilder(); - sb.append(major); - sb.append('.'); - sb.append(minor); - sb.append('.'); - sb.append(patch); - return sb.toString(); + return major + "." + minor + "." + patch; } + + /** Returns the original parsed version string (e.g. {@code "3.3.0-M1"}, {@code "3.3.0.RELEASE"}). */ @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(major); - sb.append('.'); - sb.append(minor); - sb.append('.'); - sb.append(patch); - if (qualifier != null) { - sb.append('.'); - sb.append(qualifier); - } - return sb.toString(); + return versionString; } + /** + * Compares using qualifier-aware semantic ordering: + * alpha < beta < milestone < rc < snapshot < release < service-pack. + * Four-part numeric versions are handled correctly. + *

+ * Algorithm derived from {@code org.openrewrite.semver.LatestRelease} (Apache 2.0). + */ @Override public int compareTo(Version o) { - if (major == o.major) { - if (minor == o.minor) { - return patch - o.patch; - } else { - return minor - o.minor; - } - } else { - return major - o.major; - } + int d; + if ((d = Integer.compare(major, o.major)) != 0) return d; + if ((d = Integer.compare(minor, o.minor)) != 0) return d; + if ((d = Integer.compare(patch, o.patch)) != 0) return d; + if ((d = Integer.compare(build, o.build)) != 0) return d; + if ((d = Integer.compare(releaseType.getPriority(), o.releaseType.getPriority())) != 0) return d; + return Integer.compare(qualifierNumber, o.qualifierNumber); } @Override public int hashCode() { - return Objects.hash(major, minor, patch, qualifier); + return Objects.hash(major, minor, patch, build, qualifier); } @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) + } + if (!(obj instanceof Version)) { return false; + } Version other = (Version) obj; return major == other.major && minor == other.minor && patch == other.patch - && Objects.equals(qualifier, other.qualifier); - } - - public static Version parse(String version) { - Matcher matcher = VERSION_PATTERN.matcher(version); - if (matcher.find() && matcher.groupCount() > 4) { - String major = matcher.group(1); - String minor = matcher.group(2); - String patch = matcher.group(3); - String qualifier = matcher.group(5); - return new Version( - Integer.parseInt(major), - Integer.parseInt(minor), - Integer.parseInt(patch), - qualifier - ); - } else { - String[] tokens = version.split("\\."); - if (tokens.length <= 3) { - if (tokens.length >= 1) { - int major = Integer.parseInt(tokens[0]); - if (tokens.length >= 2) { - int minor = Integer.parseInt(tokens[1]); - if (tokens.length == 3) { - int patch = Integer.parseInt(tokens[2]); - return new Version(major, minor, patch, null); - } else { - return new Version(major, minor, 0, null); - } - } else { - return new Version(major, 0, 0, null); - } - } - } + && build == other.build && Objects.equals(qualifier, other.qualifier); + } + + /** + * Parses a version string into a {@link Version}. + *

+ * Accepts formats like {@code 3.3.0}, {@code 3.3.0-M1}, {@code 3.3.0-SNAPSHOT}, + * {@code 3.3.0.RELEASE}, {@code 3.3.0.1} (four-part), {@code 2.7}, {@code 2}. + * + * @return the parsed version, or {@code null} if the string cannot be parsed + */ + public static Version parse(String versionStr) { + if (versionStr == null || versionStr.isEmpty()) { + return null; + } + Matcher m = RELEASE_PATTERN.matcher(versionStr); + if (m.matches()) { + int major = parseGroup(m, 1); + int minor = parseGroup(m, 2); + int patch = parseGroup(m, 3); + int build = parseGroup(m, 4); + String qualifier = stripSeparator(m.group(QUALIFIER_GROUP)); + return new Version(major, minor, patch, build, qualifier, versionStr); + } + // Fallback: simple dot-delimited numeric segments + String[] parts = versionStr.split("\\."); + try { + int major = Integer.parseInt(parts[0]); + int minor = parts.length >= 2 ? Integer.parseInt(parts[1]) : 0; + int patch = parts.length >= 3 ? Integer.parseInt(parts[2]) : 0; + return new Version(major, minor, patch, 0, null, versionStr); + } catch (NumberFormatException e) { + return null; } - return null; } + // ---- parsing helpers ---- + private static int parseGroup(Matcher m, int group) { + String s = m.group(group); + return s != null ? Integer.parseInt(s) : 0; + } + + private static String stripSeparator(String qualifierWithSep) { + if (qualifierWithSep == null || qualifierWithSep.isEmpty()) { + return null; + } + char sep = qualifierWithSep.charAt(0); + if (sep == '-' || sep == '.' || sep == '+') { + String text = qualifierWithSep.substring(1); + return text.isEmpty() ? null : text; + } + return qualifierWithSep; + } + + private static int toQualifierNumber(String qualifier) { + if (qualifier == null || qualifier.isEmpty()) { + return 0; + } + Matcher m = QUALIFIER_TYPE_PATTERN.matcher(qualifier); + if (m.matches()) { + String num = m.group(2); + return num.isEmpty() ? 0 : Integer.parseInt(num); + } + return 0; + } + + private static ReleaseType toReleaseType(String qualifier) { + if (qualifier == null || qualifier.isEmpty()) { + return ReleaseType.RELEASE; + } + String lower = qualifier.toLowerCase(); + if (lower.equals("release") || lower.equals("ga") || lower.equals("final")) { + return ReleaseType.RELEASE; + } + if (lower.startsWith("snapshot")) { + return ReleaseType.SNAPSHOT; + } + if (lower.startsWith("sp")) { + return ReleaseType.SERVICE_PACK; + } + Matcher m = QUALIFIER_TYPE_PATTERN.matcher(lower); + if (m.matches()) { + switch (m.group(1).toLowerCase()) { + case "snapshot": return ReleaseType.SNAPSHOT; + case "rc": case "cr": return ReleaseType.RC; + case "milestone": case "m": return ReleaseType.MILESTONE; + case "beta": case "b": return ReleaseType.BETA; + case "alpha": case "a": return ReleaseType.ALPHA; + default: break; + } + } + return ReleaseType.RELEASE; + } } diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/Classpath.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/Classpath.java index 9857150edb..04e1dbb359 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/Classpath.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/Classpath.java @@ -19,8 +19,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.ide.vscode.commons.Version; - public class Classpath { // Pattern copied from https://semver.org/ @@ -81,7 +79,7 @@ public static class CPE { private Map extra; - transient private Version version; + transient private String version; transient private String name; public CPE() {} @@ -225,7 +223,7 @@ public boolean equals(Object obj) { && Objects.equals(path, other.path) && Objects.equals(sourceContainerUrl, other.sourceContainerUrl); } - public Version getVersion() { + public String getVersion() { if (version == null) { if (ENTRY_KIND_BINARY.equals(getKind()) && !isSystem) { version = getDependencyVersion(new File(getPath()).getName()); @@ -269,14 +267,12 @@ public static boolean isProjectTestJavaSource(CPE cpe) { return isProjectJavaSource(cpe) && cpe.isTest(); } - static Version getDependencyVersion(String fileName) { + static String getDependencyVersion(String fileName) { Matcher matcher = VERSION_PATTERN.matcher(fileName); - if (matcher.find() && matcher.groupCount() > 5) { - String major = matcher.group(1); - String minor = matcher.group(2); - String patch = matcher.group(3); - String qualifier = matcher.group(5); - return new Version(Integer.parseInt(major), Integer.parseInt(minor), Integer.parseInt(patch), qualifier); + if (matcher.find()) { + // Slice out the version string directly from the filename — everything from + // the start of the major number up to (not including) ".jar". + return fileName.substring(matcher.start(1), fileName.length() - ".jar".length()); } return null; } diff --git a/headless-services/commons/commons-lsp-extensions/src/test/java/org/springframework/ide/vscode/commons/VersionTests.java b/headless-services/commons/commons-lsp-extensions/src/test/java/org/springframework/ide/vscode/commons/VersionTests.java index 6cdc2ba665..0298b383fe 100644 --- a/headless-services/commons/commons-lsp-extensions/src/test/java/org/springframework/ide/vscode/commons/VersionTests.java +++ b/headless-services/commons/commons-lsp-extensions/src/test/java/org/springframework/ide/vscode/commons/VersionTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 VMware, Inc. + * Copyright (c) 2023, 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 @@ -11,48 +11,419 @@ package org.springframework.ide.vscode.commons; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.commons.Version.ReleaseType; public class VersionTests { - - @Test - void testVersionCalculation1() throws Exception { - Version version = Version.parse("2.7.5"); - assertEquals(2, version.getMajor()); - assertEquals(7, version.getMinor()); - assertEquals(5, version.getPatch()); - assertNull(version.getQualifier()); - - version = Version.parse("3.0.0-SNAPSHOT"); - assertEquals(3, version.getMajor()); - assertEquals(0, version.getMinor()); - assertEquals(0, version.getPatch()); - assertEquals(version.getQualifier(), "SNAPSHOT"); - - - version = Version.parse("2.6.14-RC2"); - assertEquals(2, version.getMajor()); - assertEquals(6, version.getMinor()); - assertEquals(14, version.getPatch()); - assertEquals(version.getQualifier(), "RC2"); - } - - @Test - void testVersionCalculation2() throws Exception { - Version version = Version.parse("2.7"); - assertEquals(2, version.getMajor()); - assertEquals(7, version.getMinor()); - assertEquals(0, version.getPatch()); - assertNull(version.getQualifier()); - - version = Version.parse("2"); - assertEquals(2, version.getMajor()); - assertEquals(0, version.getMinor()); - assertEquals(0, version.getPatch()); - assertNull(version.getQualifier()); - } + // ---- parsing ---- + + @Test + void parseThreePartRelease() { + Version v = Version.parse("2.7.5"); + assertEquals(2, v.getMajor()); + assertEquals(7, v.getMinor()); + assertEquals(5, v.getPatch()); + assertNull(v.getQualifier()); + assertEquals(ReleaseType.RELEASE, v.getReleaseType()); + assertTrue(v.isRelease()); + assertFalse(v.isPreRelease()); + assertEquals("2.7.5", v.toString()); + } + + @Test + void parseSnapshot() { + Version v = Version.parse("3.0.0-SNAPSHOT"); + assertEquals(3, v.getMajor()); + assertEquals(0, v.getMinor()); + assertEquals(0, v.getPatch()); + assertEquals("SNAPSHOT", v.getQualifier()); + assertEquals(ReleaseType.SNAPSHOT, v.getReleaseType()); + assertTrue(v.isSnapshot()); + assertTrue(v.isPreRelease()); + assertFalse(v.isRelease()); + } + + @Test + void parseRcWithHyphen() { + Version v = Version.parse("2.6.14-RC2"); + assertEquals(2, v.getMajor()); + assertEquals(6, v.getMinor()); + assertEquals(14, v.getPatch()); + assertEquals("RC2", v.getQualifier()); + assertEquals(ReleaseType.RC, v.getReleaseType()); + assertTrue(v.isPreRelease()); + } + + @Test + void parseMilestoneWithHyphen() { + Version v = Version.parse("3.3.0-M1"); + assertEquals(3, v.getMajor()); + assertEquals(3, v.getMinor()); + assertEquals(0, v.getPatch()); + assertEquals("M1", v.getQualifier()); + assertEquals(ReleaseType.MILESTONE, v.getReleaseType()); + assertTrue(v.isPreRelease()); + } + + @Test + void parseOldStyleReleaseQualifier() { + Version v = Version.parse("2.7.5.RELEASE"); + assertEquals(2, v.getMajor()); + assertEquals(7, v.getMinor()); + assertEquals(5, v.getPatch()); + assertEquals("RELEASE", v.getQualifier()); + assertEquals(ReleaseType.RELEASE, v.getReleaseType()); + assertTrue(v.isRelease()); + } + + @Test + void parseOldStyleMilestoneQualifier() { + Version v = Version.parse("1.0.0.M1"); + assertEquals(1, v.getMajor()); + assertEquals(0, v.getMinor()); + assertEquals(0, v.getPatch()); + assertEquals("M1", v.getQualifier()); + assertEquals(ReleaseType.MILESTONE, v.getReleaseType()); + assertTrue(v.isPreRelease()); + } + + @Test + void parseOldStyleRcQualifier() { + Version v = Version.parse("2.7.5.RC1"); + assertEquals(ReleaseType.RC, v.getReleaseType()); + } + + @Test + void parseFourPartNumericVersion() { + Version v = Version.parse("3.3.0.1"); + assertNotNull(v); + assertEquals(3, v.getMajor()); + assertEquals(3, v.getMinor()); + assertEquals(0, v.getPatch()); + assertEquals(1, v.getBuild()); + assertNull(v.getQualifier()); + assertEquals(ReleaseType.RELEASE, v.getReleaseType()); + assertEquals("3.3.0.1", v.toString()); + } + + @Test + void parseFourPartWithQualifier() { + Version v = Version.parse("3.3.0.1.RELEASE"); + assertNotNull(v); + assertEquals(3, v.getMajor()); + assertEquals(3, v.getMinor()); + assertEquals(0, v.getPatch()); + assertEquals(1, v.getBuild()); + assertEquals("RELEASE", v.getQualifier()); + assertEquals(ReleaseType.RELEASE, v.getReleaseType()); + } + + @Test + void parseTwoPart() { + Version v = Version.parse("2.7"); + assertEquals(2, v.getMajor()); + assertEquals(7, v.getMinor()); + assertEquals(0, v.getPatch()); + assertNull(v.getQualifier()); + } + + @Test + void parseOnePart() { + Version v = Version.parse("2"); + assertEquals(2, v.getMajor()); + assertEquals(0, v.getMinor()); + assertEquals(0, v.getPatch()); + assertNull(v.getQualifier()); + } + + @Test + void parseGaQualifier() { + Version v = Version.parse("3.3.0.GA"); + assertEquals(ReleaseType.RELEASE, v.getReleaseType()); + assertTrue(v.isRelease()); + } + + @Test + void parseAlpha() { + Version v = Version.parse("1.0.0-ALPHA1"); + assertEquals(ReleaseType.ALPHA, v.getReleaseType()); + assertTrue(v.isPreRelease()); + } + + @Test + void parseBeta() { + Version v = Version.parse("1.0.0-BETA2"); + assertEquals(ReleaseType.BETA, v.getReleaseType()); + assertTrue(v.isPreRelease()); + } + + // ---- sorting / compareTo ---- + + @Test + void releaseGreaterThanSnapshot() { + Version release = Version.parse("3.3.0"); + Version snapshot = Version.parse("3.3.0-SNAPSHOT"); + assertTrue(release.compareTo(snapshot) > 0, "3.3.0 should be > 3.3.0-SNAPSHOT"); + assertTrue(snapshot.compareTo(release) < 0, "3.3.0-SNAPSHOT should be < 3.3.0"); + } + + @Test + void releaseGreaterThanMilestone() { + assertTrue(Version.parse("3.3.0").compareTo(Version.parse("3.3.0-M1")) > 0); + } + + @Test + void releaseGreaterThanRc() { + assertTrue(Version.parse("3.3.0").compareTo(Version.parse("3.3.0-RC1")) > 0); + } + + @Test + void rcGreaterThanMilestone() { + assertTrue(Version.parse("3.3.0-RC1").compareTo(Version.parse("3.3.0-M1")) > 0); + } + + @Test + void milestoneGreaterThanBeta() { + assertTrue(Version.parse("3.3.0-M1").compareTo(Version.parse("3.3.0-BETA1")) > 0); + } + + @Test + void betaGreaterThanAlpha() { + assertTrue(Version.parse("3.3.0-BETA1").compareTo(Version.parse("3.3.0-ALPHA1")) > 0); + } + + @Test + void laterMilestoneGreaterThanEarlier() { + assertTrue(Version.parse("3.3.0-M2").compareTo(Version.parse("3.3.0-M1")) > 0); + } + + @Test + void laterRcGreaterThanEarlier() { + assertTrue(Version.parse("3.3.0-RC2").compareTo(Version.parse("3.3.0-RC1")) > 0); + } + + @Test + void fourPartVersionSorting() { + Version v100 = Version.parse("3.3.0"); + Version v101 = Version.parse("3.3.0.1"); + assertTrue(v101.compareTo(v100) > 0, "3.3.0.1 should be > 3.3.0"); + } + + @Test + void oldStyleReleaseEquivalentToPlain() { + // LatestRelease normalises .RELEASE → treated as GA, should compare equal to plain + Version plain = Version.parse("3.3.0"); + Version withRelease = Version.parse("3.3.0.RELEASE"); + assertEquals(0, plain.compareTo(withRelease)); + } + + @Test + void newerPatchGreaterThanOlder() { + assertTrue(Version.parse("3.3.1").compareTo(Version.parse("3.3.0")) > 0); + } + + @Test + void newerMinorGreaterThanOlder() { + assertTrue(Version.parse("3.4.0").compareTo(Version.parse("3.3.0")) > 0); + } + + @Test + void newerMajorGreaterThanOlder() { + assertTrue(Version.parse("4.0.0").compareTo(Version.parse("3.3.0")) > 0); + } + + // ---- isRelease / isPreRelease ---- + + @Test + void releaseTypeClassification() { + assertTrue(Version.parse("3.3.0").isRelease()); + assertFalse(Version.parse("3.3.0").isPreRelease()); + + assertTrue(Version.parse("3.3.0-SNAPSHOT").isSnapshot()); + assertFalse(Version.parse("3.3.0-SNAPSHOT").isRelease()); + + assertTrue(Version.parse("3.3.0-M1").isPreRelease()); + assertFalse(Version.parse("3.3.0-M1").isRelease()); + + assertTrue(Version.parse("3.3.0-RC1").isPreRelease()); + assertFalse(Version.parse("3.3.0-RC1").isRelease()); + } + + // ---- display / toString ---- + + @Test + void toStringPreservesOriginalString() { + assertEquals("3.3.0-M1", Version.parse("3.3.0-M1").toString()); + assertEquals("3.3.0-SNAPSHOT", Version.parse("3.3.0-SNAPSHOT").toString()); + assertEquals("2.7.5.RELEASE", Version.parse("2.7.5.RELEASE").toString()); + assertEquals("3.3.0.1", Version.parse("3.3.0.1").toString()); + } + + @Test + void parseRoundTrip() { + assertEquals("3.3.0", Version.parse("3.3.0").toString()); + assertEquals("3.3.0.1", Version.parse("3.3.0.1").toString()); + + } + + @Test + void toMajorMinorVersionStr() { + assertEquals("3.3", Version.parse("3.3.0-M1").toMajorMinorVersionStr()); + } + + @Test + void toMajorMinorPatchVersionStr() { + assertEquals("3.3.0", Version.parse("3.3.0-M1").toMajorMinorPatchVersionStr()); + } + + // ---- comprehensive sort-based comparison tests ---- + + @Test + void sortAllThreePartVersionVariants() { + // Covers every qualifier type and numeric progression (M1 SP of the prior tier. + assertSortedOrder( + "3.3.0-SP2", // highest 3-part qualifier + "3.3.0.1-A1", // 4th numeric component beats any 3-part qualifier + "3.3.0.1-SP2", // highest 4-part qualifier + "3.3.1-A1", // higher patch beats any build/qualifier + "3.3.1-SP2", + "3.4.0-A1", // higher minor beats any patch/qualifier + "3.4.0-SP2", + "4.0.0-A1", // higher major beats everything + "4.0.0-SP2" + ); + } + + @Test + void timestampedSnapshotTreatedAsSnapshot() { + // Maven timestamped snapshots (e.g. from a remote repo) must be < the GA release. + Version tsSnap = Version.parse("4.0.2-SNAPSHOT-20250101.123456-1"); + Version snap = Version.parse("4.0.2-SNAPSHOT"); + Version fin = Version.parse("4.0.2.Final"); + assertEquals(ReleaseType.SNAPSHOT, tsSnap.getReleaseType()); + assertTrue(tsSnap.compareTo(fin) < 0, "timestamped snapshot < Final"); + assertEquals(0, tsSnap.compareTo(snap), "timestamped snapshot == plain snapshot"); + } + + @Test + void gaAliasesCompareEqualToPlainRelease() { + Version plain = Version.parse("3.3.0"); + assertEquals(0, plain.compareTo(Version.parse("3.3.0.RELEASE"))); + assertEquals(0, plain.compareTo(Version.parse("3.3.0.GA"))); + assertEquals(0, plain.compareTo(Version.parse("3.3.0.FINAL"))); + assertEquals(0, Version.parse("3.3.0.RELEASE").compareTo(Version.parse("3.3.0.GA"))); + assertEquals(0, Version.parse("3.3.0.GA").compareTo(Version.parse("3.3.0.FINAL"))); + } + + @Test + void dotSeparatorQualifiersRespectPriority() { + // Old-style dot-separated qualifiers observe the same priority order. + assertTrue(Version.parse("3.3.0.M1").compareTo(Version.parse("3.3.0.RC1")) < 0); + assertTrue(Version.parse("3.3.0.RC1").compareTo(Version.parse("3.3.0.SNAPSHOT")) < 0); + // Separator style is irrelevant — only the qualifier number matters, + // so "3.3.0-M2" (number=2) > "3.3.0.M1" (number=1). + assertTrue(Version.parse("3.3.0-M2").compareTo(Version.parse("3.3.0.M1")) > 0); + } + + @Test + void aliasesCompareEqual() { + // Short and long qualifier aliases have the same ReleaseType and number, so they compare equal. + assertEquals(0, Version.parse("3.3.0-A1").compareTo(Version.parse("3.3.0-ALPHA1"))); + assertEquals(0, Version.parse("3.3.0-B1").compareTo(Version.parse("3.3.0-BETA1"))); + assertEquals(0, Version.parse("3.3.0-CR1").compareTo(Version.parse("3.3.0-RC1"))); + } + + @Test + void caseInsensitiveVersionStringsCompareEqual() { + // Qualifier parsing is case-insensitive, so mixed-case versions compare equal. + assertEquals(0, Version.parse("3.3.0-SNAPSHOT").compareTo(Version.parse("3.3.0-snapshot"))); + assertEquals(0, Version.parse("3.3.0-RC1").compareTo(Version.parse("3.3.0-rc1"))); + assertEquals(0, Version.parse("3.3.0-M1").compareTo(Version.parse("3.3.0-m1"))); + } + private void assertSortedOrder(String... expectedOrder) { + List versions = Arrays.stream(expectedOrder) + .map(Version::parse) + .collect(Collectors.toList()); + Collections.reverse(versions); + versions.sort(null); + List actual = versions.stream().map(Version::toString).collect(Collectors.toList()); + assertEquals(List.of(expectedOrder), actual); + } } diff --git a/headless-services/commons/commons-lsp-extensions/src/test/java/org/springframework/ide/vscode/commons/protocol/java/ClasspathTests.java b/headless-services/commons/commons-lsp-extensions/src/test/java/org/springframework/ide/vscode/commons/protocol/java/ClasspathTests.java index 77ccbf13d8..1aafb45b1a 100644 --- a/headless-services/commons/commons-lsp-extensions/src/test/java/org/springframework/ide/vscode/commons/protocol/java/ClasspathTests.java +++ b/headless-services/commons/commons-lsp-extensions/src/test/java/org/springframework/ide/vscode/commons/protocol/java/ClasspathTests.java @@ -14,74 +14,30 @@ import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; -import org.springframework.ide.vscode.commons.Version; public class ClasspathTests { - + @Test void testDependencyVersionCalculation2() throws Exception { - Version version = Classpath.getDependencyVersion("spring-boot-1.2.3.jar"); - assertEquals(1, version.getMajor(), 1); - assertEquals(2, version.getMinor(), 2); - assertEquals(3, version.getPatch()); - assertNull(version.getQualifier()); - - version = Classpath.getDependencyVersion("spring-boot-1.2.3-RELEASE.jar"); - assertEquals(version.getMajor(), 1); - assertEquals(version.getMinor(), 2); - assertEquals(version.getPatch(), 3); - assertEquals(version.getQualifier(), "RELEASE"); - - version = Classpath.getDependencyVersion("spring-boot-1.2.3.RELEASE.jar"); - assertEquals(1, version.getMajor(), 1); - assertEquals(2, version.getMinor(), 2); - assertEquals(3, version.getPatch()); - assertEquals("RELEASE", version.getQualifier()); - - version = Classpath.getDependencyVersion("spring-boot-1.2.3.BUILD-SNAPSHOT.jar"); - assertEquals(1, version.getMajor(), 1); - assertEquals(2, version.getMinor(), 2); - assertEquals(3, version.getPatch()); - assertEquals("BUILD-SNAPSHOT", version.getQualifier()); - - version = Classpath.getDependencyVersion("spring-boot-actuator-1.2.3.BUILD-SNAPSHOT.jar"); - assertEquals(1, version.getMajor(), 1); - assertEquals(2, version.getMinor(), 2); - assertEquals(3, version.getPatch()); - assertEquals("BUILD-SNAPSHOT", version.getQualifier()); + assertEquals("1.2.3", Classpath.getDependencyVersion("spring-boot-1.2.3.jar")); + assertEquals("1.2.3-RELEASE", Classpath.getDependencyVersion("spring-boot-1.2.3-RELEASE.jar")); + assertEquals("1.2.3.RELEASE", Classpath.getDependencyVersion("spring-boot-1.2.3.RELEASE.jar")); + assertEquals("1.2.3.BUILD-SNAPSHOT", Classpath.getDependencyVersion("spring-boot-1.2.3.BUILD-SNAPSHOT.jar")); + assertEquals("1.2.3.BUILD-SNAPSHOT", Classpath.getDependencyVersion("spring-boot-actuator-1.2.3.BUILD-SNAPSHOT.jar")); + assertNull(Classpath.getDependencyVersion("some-library.jar")); } @Test void testDependencyNameExtraction() throws Exception { - String name = Classpath.getDependencyName("spring-boot-1.2.3.jar"); - assertEquals("spring-boot", name); - - name = Classpath.getDependencyName("spring-boot-1.2.3-RELEASE.jar"); - assertEquals("spring-boot", name); - - name = Classpath.getDependencyName("spring-boot-1.2.3.RELEASE.jar"); - assertEquals("spring-boot", name); - - name = Classpath.getDependencyName("spring-boot-1.2.3.BUILD-SNAPSHOT.jar"); - assertEquals("spring-boot", name); - - name = Classpath.getDependencyName("spring-boot-actuator-1.2.3.BUILD-SNAPSHOT.jar"); - assertEquals("spring-boot-actuator", name); - - name = Classpath.getDependencyName("commons-lang3-3.12.0.jar"); - assertEquals("commons-lang3", name); - - name = Classpath.getDependencyName("jackson-databind-2.13.4.jar"); - assertEquals("jackson-databind", name); - - // Test case where no version pattern is found - name = Classpath.getDependencyName("some-library.jar"); - assertEquals("some-library", name); - - // Test case with no .jar extension - name = Classpath.getDependencyName("some-library"); - assertEquals("some-library", name); + assertEquals("spring-boot", Classpath.getDependencyName("spring-boot-1.2.3.jar")); + assertEquals("spring-boot", Classpath.getDependencyName("spring-boot-1.2.3-RELEASE.jar")); + assertEquals("spring-boot", Classpath.getDependencyName("spring-boot-1.2.3.RELEASE.jar")); + assertEquals("spring-boot", Classpath.getDependencyName("spring-boot-1.2.3.BUILD-SNAPSHOT.jar")); + assertEquals("spring-boot-actuator", Classpath.getDependencyName("spring-boot-actuator-1.2.3.BUILD-SNAPSHOT.jar")); + assertEquals("commons-lang3", Classpath.getDependencyName("commons-lang3-3.12.0.jar")); + assertEquals("jackson-databind", Classpath.getDependencyName("jackson-databind-2.13.4.jar")); + assertEquals("some-library", Classpath.getDependencyName("some-library.jar")); + assertEquals("some-library", Classpath.getDependencyName("some-library")); } - } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java index 5307220fc1..1bea02d853 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java @@ -59,7 +59,7 @@ public AddConfigurationIfBeansPresentReconciler(QuickfixRegistry quickfixRegistr @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByName(project, "spring-context"); - return version != null && version.compareTo(new Version(3, 0, 0, null)) >= 0; + return version != null && version.compareTo(Version.parse("3.0.0")) >= 0; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AuthorizeHttpRequestsReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AuthorizeHttpRequestsReconciler.java index 3f322ce85d..2121a606d0 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AuthorizeHttpRequestsReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AuthorizeHttpRequestsReconciler.java @@ -48,7 +48,7 @@ public AuthorizeHttpRequestsReconciler(QuickfixRegistry registry) { @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByPrefix(project, "spring-security-config"); - return version != null && version.compareTo(new Version(5, 6, 0, null)) >= 0; + return version != null && version.compareTo(Version.parse("5.6.0")) >= 0; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java index abe0a6f49b..1bf636233c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java @@ -63,7 +63,7 @@ public BeanRegistrarDeclarationReconciler(QuickfixRegistry registry, SpringMetam @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByName(project, "spring-context"); - return version != null && version.compareTo(new Version(7, 0, 0, null)) >= 0; + return version != null && version.compareTo(Version.parse("7.0.0")) >= 0; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/HttpSecurityLambdaDslReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/HttpSecurityLambdaDslReconciler.java index 1d0662f1a7..40ee3ac34e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/HttpSecurityLambdaDslReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/HttpSecurityLambdaDslReconciler.java @@ -37,7 +37,7 @@ public HttpSecurityLambdaDslReconciler(QuickfixRegistry registry) { @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByPrefix(project, "spring-security-config"); - return version != null && version.compareTo(new Version(5, 2, 0, null)) >= 0; + return version != null && version.compareTo(Version.parse("5.2.0")) >= 0; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ServerHttpSecurityLambdaDslReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ServerHttpSecurityLambdaDslReconciler.java index a43c52de6b..9a55d3fc0d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ServerHttpSecurityLambdaDslReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ServerHttpSecurityLambdaDslReconciler.java @@ -34,7 +34,7 @@ public ServerHttpSecurityLambdaDslReconciler(QuickfixRegistry registry) { @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByPrefix(project, "spring-security-config"); - return version != null && version.compareTo(new Version(5, 2, 0, null)) >= 0; + return version != null && version.compareTo(Version.parse("5.2.0")) >= 0; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataCassandraContributor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataCassandraContributor.java index 0c904190dc..6d153a31d1 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataCassandraContributor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataCassandraContributor.java @@ -53,7 +53,7 @@ class SpringDataCassandraContributor implements SpringDataPropertyReferenceContr @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByPrefix(project, "spring-data-cassandra"); - return version != null && version.compareTo(new Version(5, 1, 0, null)) >= 0; + return version != null && version.compareTo(Version.parse("5.1.0-M2")) >= 0; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataCommonsContributor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataCommonsContributor.java index c77212dfd6..7a2801af24 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataCommonsContributor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataCommonsContributor.java @@ -47,7 +47,7 @@ class SpringDataCommonsContributor implements SpringDataPropertyReferenceContrib @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByPrefix(project, "spring-data-commons"); - return version != null && version.compareTo(new Version(4, 1, 0, null)) >= 0; + return version != null && version.compareTo(Version.parse("4.1.0-M2")) >= 0; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataMongoDbContributor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataMongoDbContributor.java index 31aaaef60a..8a61b3b25b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataMongoDbContributor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataMongoDbContributor.java @@ -53,7 +53,7 @@ class SpringDataMongoDbContributor implements SpringDataPropertyReferenceContrib @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByPrefix(project, "spring-data-mongodb"); - return version != null && version.compareTo(new Version(5, 1, 0, null)) >= 0; + return version != null && version.compareTo(Version.parse("5.1.0-M2")) >= 0; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataRelationalContributor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataRelationalContributor.java index c330d098d9..adb67096c7 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataRelationalContributor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/SpringDataRelationalContributor.java @@ -47,7 +47,7 @@ class SpringDataRelationalContributor implements SpringDataPropertyReferenceCont @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByPrefix(project, "spring-data-relational"); - return version != null && version.compareTo(new Version(4, 1, 0, null)) >= 0; + return version != null && version.compareTo(Version.parse("4.1.0-M2")) >= 0; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java index e8eb2f468d..7d33138e8b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java @@ -76,7 +76,7 @@ public WebSecurityConfigurerAdapterReconciler(QuickfixRegistry registry) { @Override public boolean isApplicable(IJavaProject project) { Version version = SpringProjectUtil.getDependencyVersionByPrefix(project, "spring-security-config"); - return version != null && version.compareTo(new Version(5, 7, 0, null)) >= 0 && version.compareTo(new Version(6, 1, 0, null)) < 0; + return version != null && version.compareTo(Version.parse("5.7.0")) >= 0 && version.compareTo(Version.parse("6.1.0")) < 0; } @Override 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 381cabf9c2..7a92ca6cb1 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", "*", targetVersion.toMajorMinorPatchVersionStr(), null, null, null)); - recipe.getRecipeList().add(new UpgradeParentVersion("org.springframework.boot", "spring-boot-starter-parent", targetVersion.toMajorMinorPatchVersionStr(), null, null)); + recipe.getRecipeList().add(new org.openrewrite.maven.UpgradeDependencyVersion("org.springframework.boot", "*", targetVersion.toString(), null, null, null)); + recipe.getRecipeList().add(new UpgradeParentVersion("org.springframework.boot", "spring-boot-starter-parent", targetVersion.toString(), null, null)); } if (recipe.getRecipeList().isEmpty()) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/mcp/IdeProjectEnvironment.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/mcp/IdeProjectEnvironment.java index 0315aa8e12..72f6931bea 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/mcp/IdeProjectEnvironment.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/mcp/IdeProjectEnvironment.java @@ -95,7 +95,7 @@ private Dependency createDependencyFrom(CPE cpe) { // strip version name = name.substring(0, name.lastIndexOf('-')); - String version = cpe.getVersion() != null ? cpe.getVersion().toString() : ""; + String version = cpe.getVersion() != null ? cpe.getVersion() : ""; return new Dependency(name, version); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/mcp/ProjectInformation.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/mcp/ProjectInformation.java index ca7d88b3b5..f8bada2044 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/mcp/ProjectInformation.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/mcp/ProjectInformation.java @@ -97,7 +97,7 @@ public List getResolvedProjectClasspath( return classpathEntries.stream() .filter(cpe -> Classpath.ENTRY_KIND_BINARY.equals(cpe.getKind())) .filter(cpe -> !cpe.isSystem()) - .map(cpe -> new Library(cpe.getPath(), cpe.getVersion().toString())) + .map(cpe -> new Library(cpe.getPath(), cpe.getVersion())) .toList(); } 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 index 3e4dfeb87d..32498f5281 100644 --- 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 @@ -30,7 +30,7 @@ public MavenMetadata(org.openrewrite.maven.tree.MavenMetadata rawMetadata) { for (String vStr : rawMetadata.getVersioning().getVersions()) { try { Version v = Version.parse(vStr); - if (isRelease(v)) { + if (v != null && v.isRelease()) { releases.add(v); } } catch (Exception e) { @@ -42,11 +42,6 @@ public MavenMetadata(org.openrewrite.maven.tree.MavenMetadata rawMetadata) { 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/SortedVersions.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/validation/generations/SortedVersions.java index e8f95543c4..556dda0247 100644 --- 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 @@ -28,23 +28,33 @@ public SortedVersions(Collection versions) { .collect(Collectors.toList()); } + /** + * Returns the newest GA release with the same major.minor that is strictly newer than + * {@code current}. Works correctly when {@code current} is itself a pre-release + * (e.g. {@code 3.3.0-M1} → suggests {@code 3.3.0}). + */ public Optional getNewerLatestPatchRelease(Version current) { return descendingVersions.stream() + .filter(Version::isRelease) .filter(v -> v.getMajor() == current.getMajor() && v.getMinor() == current.getMinor() - && v.getPatch() > current.getPatch()) + && v.compareTo(current) > 0) .findFirst(); } + /** Returns the newest GA release with the same major version but a higher minor. */ public Optional getNewerLatestMinorRelease(Version current) { return descendingVersions.stream() + .filter(Version::isRelease) .filter(v -> v.getMajor() == current.getMajor() && v.getMinor() > current.getMinor()) .findFirst(); } + /** Returns the newest GA release with a higher major version. */ public Optional getNewerLatestMajorRelease(Version current) { return descendingVersions.stream() + .filter(Version::isRelease) .filter(v -> v.getMajor() > current.getMajor()) .findFirst(); } 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 index 5edbdce368..d4e5b87cb1 100644 --- 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 @@ -21,57 +21,57 @@ 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) + Version.parse("1.5.3.RELEASE"), + Version.parse("1.5.10.RELEASE"), + Version.parse("1.5.12.RELEASE"), + Version.parse("2.0.0"), + Version.parse("2.0.3"), + Version.parse("2.1.6"), + Version.parse("2.4.15"), + Version.parse("2.5.21"), + Version.parse("2.6.10"), + Version.parse("2.6.18"), + Version.parse("2.7.3"), + Version.parse("2.7.6"), + Version.parse("3.0.0") ); - + 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()); + assertEquals(Optional.of(Version.parse("1.5.12.RELEASE")), sortedVersions.getNewerLatestPatchRelease(Version.parse("1.5.3.RELEASE"))); + assertFalse(sortedVersions.getNewerLatestPatchRelease(Version.parse("1.4.3")).isPresent()); + assertEquals(Optional.of(Version.parse("2.1.6")), sortedVersions.getNewerLatestPatchRelease(Version.parse("2.1.3"))); + assertEquals(Optional.of(Version.parse("2.6.18")), sortedVersions.getNewerLatestPatchRelease(Version.parse("2.6.5"))); + assertFalse(sortedVersions.getNewerLatestPatchRelease(Version.parse("2.6.18")).isPresent()); + assertFalse(sortedVersions.getNewerLatestPatchRelease(Version.parse("3.0.0")).isPresent()); + assertFalse(sortedVersions.getNewerLatestPatchRelease(Version.parse("3.0.1")).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()); + assertFalse(sortedVersions.getNewerLatestMinorRelease(Version.parse("1.5.3.RELEASE")).isPresent()); + assertEquals(Optional.of(Version.parse("1.5.12.RELEASE")), sortedVersions.getNewerLatestMinorRelease(Version.parse("1.4.3.RELEASE"))); + assertEquals(Optional.of(Version.parse("2.7.6")), sortedVersions.getNewerLatestMinorRelease(Version.parse("2.1.3"))); + assertEquals(Optional.of(Version.parse("2.7.6")), sortedVersions.getNewerLatestMinorRelease(Version.parse("2.6.5"))); + assertFalse(sortedVersions.getNewerLatestMinorRelease(Version.parse("2.7.3")).isPresent()); + assertFalse(sortedVersions.getNewerLatestMinorRelease(Version.parse("2.7.6")).isPresent()); + assertFalse(sortedVersions.getNewerLatestMinorRelease(Version.parse("3.0.0")).isPresent()); + assertFalse(sortedVersions.getNewerLatestMinorRelease(Version.parse("3.0.1")).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()); + assertEquals(Optional.of(Version.parse("3.0.0")), sortedVersions.getNewerLatestMajorRelease(Version.parse("1.5.3.RELEASE"))); + assertEquals(Optional.of(Version.parse("3.0.0")), sortedVersions.getNewerLatestMajorRelease(Version.parse("1.4.3"))); + assertEquals(Optional.of(Version.parse("3.0.0")), sortedVersions.getNewerLatestMajorRelease(Version.parse("2.1.3"))); + assertEquals(Optional.of(Version.parse("3.0.0")), sortedVersions.getNewerLatestMajorRelease(Version.parse("2.6.5"))); + assertFalse(sortedVersions.getNewerLatestMajorRelease(Version.parse("3.0.0")).isPresent()); + assertFalse(sortedVersions.getNewerLatestMajorRelease(Version.parse("3.0.1")).isPresent()); + assertFalse(sortedVersions.getNewerLatestMajorRelease(Version.parse("4.1.3")).isPresent()); } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/test/SpringCloudCompatibilityValidationTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/test/SpringCloudCompatibilityValidationTest.java index 390c39196d..50fff059dc 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/test/SpringCloudCompatibilityValidationTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/validation/test/SpringCloudCompatibilityValidationTest.java @@ -161,7 +161,7 @@ private CPE createCPE(String kind, String name, String path, String version) { CPE cpe = mock(CPE.class); when(cpe.getKind()).thenReturn(kind); when(cpe.getPath()).thenReturn(path); - when(cpe.getVersion()).thenReturn(Version.parse(version)); + when(cpe.getVersion()).thenReturn(version); when(cpe.isSystem()).thenReturn(false); when(cpe.getName()).thenReturn(name); return cpe;