This is page 2 of 2. Use http://codebase.md/arvindand/maven-tools-mcp?page={x} to view the full context.
# Directory Structure
```
├── .dockerignore
├── .gitattributes
├── .github
│ └── workflows
│ ├── ci.yml
│ └── docker.yml
├── .gitignore
├── .mvn
│ └── wrapper
│ └── maven-wrapper.properties
├── assets
│ └── demo.gif
├── build
│ ├── build-docker.cmd
│ ├── build-docker.sh
│ ├── build.cmd
│ ├── build.sh
│ └── README.md
├── CHANGELOG.md
├── CORPORATE-CERTIFICATES.md
├── docker-compose.yml
├── LICENSE
├── mvnw
├── mvnw.cmd
├── pom.xml
├── README.md
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── arvindand
│ │ └── mcp
│ │ └── maven
│ │ ├── config
│ │ │ ├── CacheConfig.java
│ │ │ ├── CacheConstants.java
│ │ │ ├── Context7Properties.java
│ │ │ ├── HttpClientConfig.java
│ │ │ ├── JacksonConfig.java
│ │ │ ├── MavenCentralProperties.java
│ │ │ ├── McpToolsConfig.java
│ │ │ └── NativeImageConfiguration.java
│ │ ├── MavenMcpServerApplication.java
│ │ ├── model
│ │ │ ├── BulkCheckResult.java
│ │ │ ├── Context7Guidance.java
│ │ │ ├── DependencyAge.java
│ │ │ ├── DependencyAgeAnalysis.java
│ │ │ ├── DependencyInfo.java
│ │ │ ├── MavenArtifact.java
│ │ │ ├── MavenCoordinate.java
│ │ │ ├── MavenMetadata.java
│ │ │ ├── McpError.java
│ │ │ ├── ProjectHealthAnalysis.java
│ │ │ ├── ReleasePatternAnalysis.java
│ │ │ ├── StabilityFilter.java
│ │ │ ├── ToolResponse.java
│ │ │ ├── VersionComparison.java
│ │ │ ├── VersionInfo.java
│ │ │ ├── VersionsByType.java
│ │ │ └── VersionTimelineAnalysis.java
│ │ ├── service
│ │ │ ├── MavenCentralException.java
│ │ │ ├── MavenCentralService.java
│ │ │ └── MavenDependencyTools.java
│ │ └── util
│ │ ├── MavenCoordinateParser.java
│ │ └── VersionComparator.java
│ └── resources
│ ├── application-docker.yaml
│ ├── application-no-context7.yaml
│ ├── application.yaml
│ ├── logback-spring.xml
│ └── META-INF
│ └── additional-spring-configuration-metadata.json
└── test
├── java
│ └── com
│ └── arvindand
│ └── mcp
│ └── maven
│ ├── config
│ │ └── Context7PropertiesTest.java
│ ├── MavenMcpServerIT.java
│ ├── model
│ │ └── Context7GuidanceTest.java
│ ├── service
│ │ ├── MavenCentralServiceRepositoryIT.java
│ │ ├── MavenCentralServiceUnitTest.java
│ │ ├── MavenDependencyToolsContext7EnabledIT.java
│ │ ├── MavenDependencyToolsIT.java
│ │ └── MavenDependencyToolsPerformanceIT.java
│ ├── TestHelpers.java
│ └── util
│ ├── MavenCoordinateParserTest.java
│ └── VersionComparatorTest.java
└── resources
└── application-test.yaml
```
# Files
--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/util/VersionComparator.java:
--------------------------------------------------------------------------------
```java
package com.arvindand.mcp.maven.util;
import com.arvindand.mcp.maven.model.VersionInfo.VersionType;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.springframework.stereotype.Component;
/**
* Utility for comparing and analyzing Maven version strings.
*
* @author Arvind Menon
* @since 0.1.0
*/
@Component
public final class VersionComparator implements Comparator<String> {
private static final String UNKNOWN = "unknown";
private static final String ALPHA = "alpha";
private static final String BETA = "beta";
private static final String MILESTONE = "milestone";
private static final String PATCH = "patch";
private static final Set<String> STABLE_QUALIFIERS = Set.of("final", "ga", "release");
private static final Set<String> ALPHA_QUALIFIERS = Set.of(ALPHA, "a");
private static final Set<String> BETA_QUALIFIERS = Set.of(BETA, "b");
private static final Set<String> MILESTONE_QUALIFIERS = Set.of(MILESTONE, "m");
private static final Set<String> RC_QUALIFIERS = Set.of("rc", "cr");
/**
* Compares two version strings using Maven's ComparableVersion.
*
* @param version1 first version to compare
* @param version2 second version to compare
* @return negative if version1 < version2, 0 if equal, positive if version1 > version2
*/
@Override
public int compare(String version1, String version2) {
return switch ((version1 == null ? 1 : 0) + (version2 == null ? 2 : 0)) {
case 0 ->
new ComparableVersion(version1)
.compareTo(new ComparableVersion(version2)); // both non-null
case 1 -> -1; // version1 is null, version2 is not
case 2 -> 1; // version2 is null, version1 is not
case 3 -> 0; // both null
default -> throw new IllegalStateException("Unexpected comparison state");
};
}
/**
* Gets the latest version from an array of version strings.
*
* @param versions array of version strings
* @return the latest version or null if array is empty
*/
public static String getLatest(String[] versions) {
if (versions == null || versions.length == 0) return null;
return Arrays.stream(versions).max(new VersionComparator()).orElse(null);
}
/**
* Determines the type of update between current and latest versions.
*
* @param currentVersion the current version
* @param latestVersion the latest available version
* @return update type: "major", "minor", "patch", "none", or "unknown"
*/
public String determineUpdateType(String currentVersion, String latestVersion) {
return switch (validateVersions(currentVersion, latestVersion)) {
case INVALID -> UNKNOWN;
case EQUAL -> "none";
case VALID -> {
int comparison = compare(currentVersion, latestVersion);
if (comparison >= 0) {
yield comparison == 0 ? "none" : UNKNOWN;
} else {
yield determineUpdateType(parseVersion(currentVersion), parseVersion(latestVersion));
}
}
};
}
/**
* Checks if a version is considered stable (not pre-release).
*
* @param version the version to check
* @return true if the version is stable
*/
public boolean isStableVersion(String version) {
if (version == null) return false;
return switch (classifyQualifier(extractQualifier(version))) {
case STABLE -> true;
case PRE_RELEASE -> false;
};
}
/**
* Gets the version type enum for a version string.
*
* @param version the version to analyze
* @return the version type enum
*/
public VersionType getVersionType(String version) {
if (version == null) return VersionType.STABLE;
return switch (classifyQualifier(extractQualifier(version))) {
case STABLE -> VersionType.STABLE;
case PRE_RELEASE -> determinePreReleaseVersionType(extractQualifier(version));
};
}
/**
* Gets the version type as a display string.
*
* @param version the version to analyze
* @return the version type as a string
*/
public String getVersionTypeString(String version) {
if (version == null) return UNKNOWN;
return getVersionType(version).getDisplayName();
}
/**
* Parses a version string into numeric components and qualifier.
*
* @param version the version string to parse
* @return parsed version components
*/
public VersionComponents parseVersion(String version) {
if (version == null || version.trim().isEmpty()) {
return new VersionComponents(new int[0], "");
}
String trimmed = version.trim();
// Split on first hyphen to separate numeric part from qualifier
int hyphenIndex = findFirstQualifierSeparator(trimmed);
String numericPart = hyphenIndex != -1 ? trimmed.substring(0, hyphenIndex) : trimmed;
String qualifier = hyphenIndex != -1 ? trimmed.substring(hyphenIndex + 1).toLowerCase() : "";
// Parse numeric components (major.minor.patch.etc)
String[] segments = numericPart.split("\\.");
int[] numericParts = new int[segments.length];
for (int i = 0; i < segments.length; i++) {
try {
// Handle cases like "1.0.0-SNAPSHOT" where hyphen is within a segment
String segment = segments[i];
int segmentHyphen = segment.indexOf('-');
if (segmentHyphen != -1) {
segment = segment.substring(0, segmentHyphen);
// If this is the first time we see a qualifier, capture it
if (qualifier.isEmpty()) {
qualifier = segments[i].substring(segmentHyphen + 1).toLowerCase();
}
}
numericParts[i] = Integer.parseInt(segment);
} catch (NumberFormatException _) {
numericParts[i] = 0;
}
}
return new VersionComponents(numericParts, qualifier);
}
private int findFirstQualifierSeparator(String version) {
// Look for common qualifier separators: - or _
// But be smart about it - avoid separating dates (2023-01-15) or similar patterns
int hyphenIndex = version.indexOf('-');
int underscoreIndex = version.indexOf('_');
// Return the first separator found, preferring hyphen
if (hyphenIndex == -1 && underscoreIndex == -1) return -1;
if (hyphenIndex == -1) return underscoreIndex;
if (underscoreIndex == -1) return hyphenIndex;
return Math.min(hyphenIndex, underscoreIndex);
}
private String extractQualifier(String version) {
return parseVersion(version).qualifier();
}
private ValidationResult validateVersions(String current, String latest) {
if (current == null || latest == null) return ValidationResult.INVALID;
if (current.equals(latest)) return ValidationResult.EQUAL;
return ValidationResult.VALID;
}
private QualifierType classifyQualifier(String qualifier) {
if (qualifier.isEmpty() || STABLE_QUALIFIERS.contains(qualifier)) {
return QualifierType.STABLE;
}
String lower = qualifier.toLowerCase();
// Treat service packs as stable (e.g., 1.0.0-SP1)
if (lower.startsWith("sp")) {
return QualifierType.STABLE;
}
// Common pre-release markers
if (lower.contains("snapshot")
|| lower.contains("rc")
|| lower.contains("cr")
|| lower.contains("m")
|| lower.contains(BETA)
|| lower.contains(ALPHA)
|| lower.contains("preview")
|| lower.contains("dev")) {
return QualifierType.PRE_RELEASE;
}
// Unknown qualifier: conservatively treat as pre-release rather than stable
return QualifierType.PRE_RELEASE;
}
private VersionType determinePreReleaseVersionType(String qualifier) {
if (isAlphaQualifier(qualifier)) return VersionType.ALPHA;
if (isBetaQualifier(qualifier)) return VersionType.BETA;
if (isMilestoneQualifier(qualifier)) return VersionType.MILESTONE;
if (isRcQualifier(qualifier)) return VersionType.RC;
return VersionType.ALPHA; // Default unknown pre-releases to alpha
}
private boolean isAlphaQualifier(String qualifier) {
return ALPHA_QUALIFIERS.contains(qualifier)
|| qualifier.startsWith(ALPHA)
|| qualifier.startsWith("a")
|| qualifier.contains("dev")
|| qualifier.contains("preview");
}
private boolean isBetaQualifier(String qualifier) {
return BETA_QUALIFIERS.contains(qualifier)
|| qualifier.startsWith(BETA)
|| qualifier.startsWith("b");
}
private boolean isMilestoneQualifier(String qualifier) {
return MILESTONE_QUALIFIERS.contains(qualifier)
|| qualifier.startsWith(MILESTONE)
|| qualifier.startsWith("m");
}
private boolean isRcQualifier(String qualifier) {
return RC_QUALIFIERS.contains(qualifier)
|| qualifier.startsWith("rc")
|| qualifier.startsWith("cr")
|| qualifier.contains("candidate");
}
private String determineUpdateType(VersionComponents current, VersionComponents latest) {
// Handle case where numeric parts are identical but qualifiers differ
boolean sameNumericVersion = areNumericVersionsEqual(current, latest);
if (sameNumericVersion) {
// If numeric versions are the same, check qualifiers
return determineQualifierUpdate(current.qualifier(), latest.qualifier());
}
// Compare numeric parts to determine update type
int maxLength = Math.max(current.numericParts().length, latest.numericParts().length);
for (int i = 0; i < maxLength; i++) {
int currentPart = i < current.numericParts().length ? current.numericParts()[i] : 0;
int latestPart = i < latest.numericParts().length ? latest.numericParts()[i] : 0;
if (latestPart > currentPart) {
return switch (i) {
case 0 -> "major";
case 1 -> "minor";
default -> PATCH;
};
} else if (currentPart > latestPart) {
// Current version is higher than "latest" - this is a downgrade scenario
return UNKNOWN;
}
}
return "none";
}
private boolean areNumericVersionsEqual(VersionComponents current, VersionComponents latest) {
int maxLength = Math.max(current.numericParts().length, latest.numericParts().length);
for (int i = 0; i < maxLength; i++) {
int currentPart = i < current.numericParts().length ? current.numericParts()[i] : 0;
int latestPart = i < latest.numericParts().length ? latest.numericParts()[i] : 0;
if (currentPart != latestPart) {
return false;
}
}
return true;
}
private String determineQualifierUpdate(String currentQualifier, String latestQualifier) {
// If both are empty, versions are identical
if (currentQualifier.isEmpty() && latestQualifier.isEmpty()) {
return "none";
}
// If current has qualifier but latest doesn't, it's upgrading to stable
if (!currentQualifier.isEmpty() && latestQualifier.isEmpty()) {
return PATCH; // Treat pre-release to stable as patch update
}
// If current is stable but latest has qualifier, this is unusual (downgrade to pre-release)
if (currentQualifier.isEmpty() && !latestQualifier.isEmpty()) {
return UNKNOWN;
}
// Both have qualifiers - compare stability levels
return compareQualifierStability(currentQualifier, latestQualifier);
}
private String compareQualifierStability(String currentQualifier, String latestQualifier) {
// Define stability order (lower index = less stable)
String[] stabilityOrder = {ALPHA, BETA, MILESTONE, "rc", "snapshot"};
int currentStability = getQualifierStability(currentQualifier, stabilityOrder);
int latestStability = getQualifierStability(latestQualifier, stabilityOrder);
if (latestStability > currentStability) {
return PATCH; // Upgrading to more stable pre-release
} else if (latestStability < currentStability) {
return UNKNOWN; // Downgrading to less stable
} else {
// Same stability level - might be version number change within qualifier
return currentQualifier.equals(latestQualifier) ? "none" : PATCH;
}
}
private int getQualifierStability(String qualifier, String[] stabilityOrder) {
String lowerQualifier = qualifier.toLowerCase();
for (int i = 0; i < stabilityOrder.length; i++) {
if (lowerQualifier.contains(stabilityOrder[i])) {
return i;
}
}
// Unknown qualifier types get medium stability
return stabilityOrder.length / 2;
}
/**
* Represents parsed version components.
*
* @param numericParts the numeric parts of the version
* @param qualifier the qualifier part (e.g., "alpha", "beta")
*/
public record VersionComponents(int[] numericParts, String qualifier) {
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
VersionComponents that = (VersionComponents) obj;
return Arrays.equals(numericParts, that.numericParts)
&& Objects.equals(qualifier, that.qualifier);
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(numericParts), qualifier);
}
@Override
public String toString() {
return "VersionComponents{numericParts="
+ Arrays.toString(numericParts)
+ ", qualifier='"
+ qualifier
+ "'}";
}
}
private enum ValidationResult {
INVALID,
EQUAL,
VALID
}
private enum QualifierType {
STABLE,
PRE_RELEASE
}
}
```
--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/util/VersionComparatorTest.java:
--------------------------------------------------------------------------------
```java
package com.arvindand.mcp.maven.util;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Comprehensive unit tests for VersionComparator.
*
* @author Arvind Menon
* @since 0.1.0
*/
class VersionComparatorTest {
private VersionComparator versionComparator;
@BeforeEach
void setUp() {
versionComparator = new VersionComparator();
}
/** Test data for version comparison. */
private static Stream<Arguments> versionComparisonTestData() {
return Stream.of(
// Basic numeric comparisons
Arguments.of("1.0.0", "2.0.0", -1, "1.0.0 < 2.0.0"),
Arguments.of("2.0.0", "1.0.0", 1, "2.0.0 > 1.0.0"),
Arguments.of("1.0.0", "1.0.0", 0, "1.0.0 = 1.0.0"),
// Minor version comparisons
Arguments.of("1.0.0", "1.1.0", -1, "1.0.0 < 1.1.0"),
Arguments.of("1.5.0", "1.2.0", 1, "1.5.0 > 1.2.0"),
// Patch version comparisons
Arguments.of("1.0.0", "1.0.1", -1, "1.0.0 < 1.0.1"),
Arguments.of("1.0.5", "1.0.2", 1, "1.0.5 > 1.0.2"),
// Different length versions - should be treated as equal
Arguments.of("1.0", "1.0.0", 0, "1.0 = 1.0.0 (equivalent versions)"),
Arguments.of("1.0.0", "1.0", 0, "1.0.0 = 1.0 (equivalent versions)"),
Arguments.of("1", "1.0.0", 0, "1 = 1.0.0 (equivalent versions)"),
Arguments.of("1.0", "1.0.1", -1, "1.0 < 1.0.1"),
Arguments.of("1.1", "1.0.1", 1, "1.1 > 1.0.1"),
// Large numbers
Arguments.of("10.0.0", "9.9.9", 1, "10.0.0 > 9.9.9"),
Arguments.of("1.10.0", "1.9.0", 1, "1.10.0 > 1.9.0"),
Arguments.of("1.0.10", "1.0.9", 1, "1.0.10 > 1.0.9"),
// Multi-digit versions
Arguments.of("1.2.3", "1.2.10", -1, "1.2.3 < 1.2.10"),
Arguments.of(
"11.0.0",
"2.0.0",
1,
"11.0.0 > 2.0.0"), // Qualifier comparisons (release > rc > milestone > beta > alpha)
Arguments.of("1.0.0", "1.0.0-RC1", 1, "release > rc"),
Arguments.of("1.0.0-RC1", "1.0.0-M1", 1, "rc > milestone"),
Arguments.of("1.0.0-M1", "1.0.0-BETA1", 1, "milestone > beta"),
Arguments.of("1.0.0-BETA1", "1.0.0-ALPHA1", 1, "beta > alpha"),
// Same qualifier with different numbers
Arguments.of("1.0.0-RC1", "1.0.0-RC2", -1, "RC1 < RC2"),
Arguments.of("1.0.0-BETA2", "1.0.0-BETA1", 1, "BETA2 > BETA1"),
Arguments.of("1.0.0-M2", "1.0.0-M1", 1, "M2 > M1"), // Different separators
Arguments.of("1.0.0", "1-0-0", 0, "Different separators treated equally"),
Arguments.of("1.0.0", "1_0_0", -1, "Underscore separators (Maven treats differently)"),
Arguments.of(
"1.0.0-RC1",
"1.0.0.RC1",
0,
"Different qualifier separators (Maven treats as equivalent)"),
// Case insensitive qualifiers
Arguments.of("1.0.0-rc1", "1.0.0-RC1", 0, "Case insensitive RC"),
Arguments.of("1.0.0-beta", "1.0.0-BETA", 0, "Case insensitive BETA"),
Arguments.of("1.0.0-alpha", "1.0.0-ALPHA", 0, "Case insensitive ALPHA"),
// Special qualifiers - Maven's official behavior
Arguments.of("1.0.0.RELEASE", "1.0.0", 0, "RELEASE qualifier equivalent to plain"),
Arguments.of("1.0.0.Final", "1.0.0", 0, "Final qualifier equivalent to plain"),
Arguments.of("1.0.0.GA", "1.0.0", 0, "GA qualifier equivalent to plain"),
// Complex real-world examples
Arguments.of("3.2.0", "3.2.0-RC1", 1, "Spring Boot style"),
Arguments.of("2.15.2", "2.16.0-SNAPSHOT", -1, "Jackson style with snapshot"),
Arguments.of("6.1.4", "6.1.4.RELEASE", 0, "Spring Framework style"));
}
@ParameterizedTest(name = "{3}")
@MethodSource("versionComparisonTestData")
void testCompare(String version1, String version2, int expectedSign, String description) {
// When
int result = versionComparator.compare(version1, version2);
// Then - Check the sign of the result
if (expectedSign == 0) {
assertThat(result).isZero();
} else if (expectedSign < 0) {
assertThat(result).isNegative();
} else {
assertThat(result).isPositive();
}
}
@Test
void testCompare_NullAndEmptyVersions() {
// Null versions should be handled gracefully or throw appropriate exceptions
// The exact behavior depends on implementation, but it should be consistent
try {
int result1 = versionComparator.compare(null, "1.0.0");
int result2 = versionComparator.compare("1.0.0", null);
int result3 = versionComparator.compare(null, null);
// If no exception, results should be consistent
assertThat(result1).isNotEqualTo(result2); // Should be opposites
assertThat(result3).isZero(); // null equals null
} catch (NullPointerException e) {
// This is also acceptable behavior
assertThat(e).isInstanceOf(NullPointerException.class);
}
}
@Test
void testCompare_SameVersionsAreEqual() {
String[] versions = {"1.0.0", "2.5.1", "1.0.0-RC1", "1.0.0-BETA", "1.0.0-ALPHA", "1.0.0-M1"};
for (String version : versions) {
int result = versionComparator.compare(version, version);
assertThat(result).isZero();
}
}
@Test
void testGetLatest_BasicVersions() {
// Given
String[] versions = {"1.0.0", "2.0.0", "1.5.0", "2.1.0", "1.0.1"};
// When
String latest = VersionComparator.getLatest(versions);
// Then
assertThat(latest).isEqualTo("2.1.0");
}
@Test
void testGetLatest_WithQualifiers() {
// Given
String[] versions = {"1.0.0", "1.0.0-RC1", "1.0.0-BETA", "1.0.0-ALPHA", "1.0.0-M1"};
// When
String latest = VersionComparator.getLatest(versions);
// Then - Release version should be latest
assertThat(latest).isEqualTo("1.0.0");
}
@Test
void testGetLatest_RealWorldExample() {
// Given - Realistic Spring Boot versions
String[] versions = {
"3.1.0", "3.1.1", "3.1.2", "3.2.0-RC1", "3.2.0-SNAPSHOT", "3.2.0", "3.1.5", "3.0.12"
};
// When
String latest = VersionComparator.getLatest(versions);
// Then - Should be the highest stable release
assertThat(latest).isEqualTo("3.2.0");
}
@Test
void testGetLatest_EmptyArray() {
// Given
String[] versions = {};
// When
String latest = VersionComparator.getLatest(versions);
// Then
assertThat(latest).isNull();
}
@Test
void testGetLatest_NullArray() {
// Given
String[] versions = null;
// When
String latest = VersionComparator.getLatest(versions);
// Then
assertThat(latest).isNull();
}
@Test
void testGetLatest_SingleVersion() {
// Given
String[] versions = {"1.0.0"};
// When
String latest = VersionComparator.getLatest(versions);
// Then
assertThat(latest).isEqualTo("1.0.0");
}
@Test
void testSorting_DescendingOrder() {
// Given
List<String> versions = Arrays.asList("1.0.0", "3.0.0", "2.0.0", "1.5.0", "2.1.0");
// When - Sort in descending order
List<String> sorted = versions.stream().sorted(versionComparator.reversed()).toList();
// Then
assertThat(sorted).containsExactly("3.0.0", "2.1.0", "2.0.0", "1.5.0", "1.0.0");
}
@Test
void testSorting_AscendingOrder() {
// Given
List<String> versions = Arrays.asList("3.0.0", "1.0.0", "2.1.0", "1.5.0", "2.0.0");
// When - Sort in ascending order
List<String> sorted = versions.stream().sorted(versionComparator).toList();
// Then
assertThat(sorted).containsExactly("1.0.0", "1.5.0", "2.0.0", "2.1.0", "3.0.0");
}
@Test
void testQualifierPriority() {
// Given - Same base version with different qualifiers
List<String> versions =
Arrays.asList("1.0.0-M1", "1.0.0-ALPHA", "1.0.0-BETA", "1.0.0-RC1", "1.0.0");
// When - Sort in ascending order
List<String> sorted =
versions.stream()
.sorted(versionComparator)
.toList(); // Then - Should be in Maven order: alpha < beta < milestone < rc < release
assertThat(sorted)
.containsExactly("1.0.0-ALPHA", "1.0.0-BETA", "1.0.0-M1", "1.0.0-RC1", "1.0.0");
}
@Test
void testMixedVersionFormats() {
// Given - Versions with different formats
List<String> versions = Arrays.asList("1", "1.0", "1.0.0", "1.0.1", "1.1", "2");
// When - Sort in ascending order
List<String> sorted = versions.stream().sorted(versionComparator).toList();
// Then - Should handle different formats correctly
assertThat(sorted).containsExactly("1", "1.0", "1.0.0", "1.0.1", "1.1", "2");
}
@Test
void testConsistencyWithEquals() {
// Given - Versions that should be equal
String[][] equalVersionPairs = {
{"1.0.0", "1.0.0"},
{"1.0", "1.0.0"}, // Assuming this should be equal
{"1.0.0-RC1", "1.0.0-rc1"}
};
for (String[] pair : equalVersionPairs) {
// When
int result1 = versionComparator.compare(pair[0], pair[1]);
int result2 = versionComparator.compare(pair[1], pair[0]);
// Then - If equal, both comparisons should return 0
if (result1 == 0) {
assertThat(result2).isZero();
} else {
// If not equal, they should be opposites
assertThat(result1 * result2).isNegative();
}
}
}
@Test
void testTransitivity() {
// Given - Three versions
String version1 = "1.0.0";
String version2 = "1.5.0";
String version3 = "2.0.0";
// When
int compare12 = versionComparator.compare(version1, version2);
int compare23 = versionComparator.compare(version2, version3);
int compare13 = versionComparator.compare(version1, version3);
// Then - If version1 < version2 and version2 < version3, then version1 < version3
if (compare12 < 0 && compare23 < 0) {
assertThat(compare13).isNegative();
}
}
@Test
void testLargeVersionNumbers() {
// Given - Versions with large numbers
String[] versions = {"999.999.999", "1000.0.0", "1000.1.0"};
// When
String latest = VersionComparator.getLatest(versions);
// Then
assertThat(latest).isEqualTo("1000.1.0");
}
@Test
void testVersionsWithLeadingZeros() {
// Given - Versions that might have leading zeros
String version1 = "1.01.0";
String version2 = "1.1.0";
// When
int result = versionComparator.compare(version1, version2);
// Then - Should handle leading zeros correctly (01 should equal 1)
assertThat(result).isZero();
}
@ParameterizedTest
@MethodSource("updateTypeTestData")
void testDetermineUpdateType(
String current, String latest, String expectedUpdateType, String description) {
// When
String updateType = versionComparator.determineUpdateType(current, latest);
// Then
assertThat(updateType).as(description).isEqualTo(expectedUpdateType);
}
@Test
void testCriticalVersionParsingScenarios() {
// Test the critical parsing scenarios that were failing before the fix
// Spring Boot milestone versions (previously corrupted by ComparableVersion.toString())
assertThat(versionComparator.determineUpdateType("2.0.0-M1", "2.0.0")).isEqualTo("patch");
assertThat(versionComparator.determineUpdateType("1.5.0", "2.0.0-M1")).isEqualTo("major");
// Complex pre-release versions
assertThat(versionComparator.determineUpdateType("3.1.0-RC1", "3.1.0")).isEqualTo("patch");
assertThat(versionComparator.determineUpdateType("1.0.0-alpha", "1.0.0-beta"))
.isEqualTo("patch");
// Verify version parsing doesn't corrupt the original strings
VersionComparator.VersionComponents parsed1 = versionComparator.parseVersion("2.0.0-M1");
assertThat(parsed1.qualifier()).isEqualTo("m1");
assertThat(parsed1.numericParts()).containsExactly(2, 0, 0);
VersionComparator.VersionComponents parsed2 = versionComparator.parseVersion("1.0.0-SNAPSHOT");
assertThat(parsed2.qualifier()).isEqualTo("snapshot");
assertThat(parsed2.numericParts()).containsExactly(1, 0, 0);
}
private static Stream<Arguments> updateTypeTestData() {
return Stream.of(
// Major updates
Arguments.of("1.0.0", "2.0.0", "major", "Major version update"),
Arguments.of("1.5.3", "2.0.0", "major", "Major version update with minor/patch"),
Arguments.of("1.0.0-alpha", "2.0.0", "major", "Major update from alpha"),
// Minor updates
Arguments.of("1.0.0", "1.1.0", "minor", "Minor version update"),
Arguments.of("1.0.5", "1.2.0", "minor", "Minor update with patch difference"),
Arguments.of("1.0.0-beta", "1.1.0", "minor", "Minor update from beta"),
// Patch updates
Arguments.of("1.0.0", "1.0.1", "patch", "Patch version update"),
Arguments.of("1.2.3", "1.2.4", "patch", "Simple patch update"),
Arguments.of("1.0.0-rc", "1.0.1", "patch", "Patch update from RC"),
// No updates / equal versions
Arguments.of("1.0.0", "1.0.0", "none", "Same version"),
Arguments.of("1.0.0-alpha", "1.0.0-alpha", "none", "Same alpha version"),
// Unknown/downgrade scenarios
Arguments.of("2.0.0", "1.0.0", "unknown", "Downgrade scenario"),
Arguments.of("1.1.0", "1.0.0", "unknown", "Minor downgrade"),
Arguments.of(null, "1.0.0", "unknown", "Null current version"),
Arguments.of("1.0.0", null, "unknown", "Null latest version"));
}
@ParameterizedTest
@MethodSource("stableVersionTestData")
void testIsStableVersion(String version, boolean expectedStable, String description) {
// When
boolean isStable = versionComparator.isStableVersion(version);
// Then
assertThat(isStable).as(description).isEqualTo(expectedStable);
}
private static Stream<Arguments> stableVersionTestData() {
return Stream.of(
// Stable versions
Arguments.of("1.0.0", true, "Plain numeric version is stable"),
Arguments.of("2.1.5", true, "Multi-component numeric version is stable"),
Arguments.of("1.0.0-final", true, "Final qualifier is stable"),
Arguments.of("1.0.0-ga", true, "GA qualifier is stable"),
Arguments.of("1.0.0-release", true, "Release qualifier is stable"),
Arguments.of("1.0.0-sp1", true, "Service pack is stable"),
// Pre-release versions
Arguments.of("1.0.0-alpha", false, "Alpha version is not stable"),
Arguments.of("1.0.0-beta", false, "Beta version is not stable"),
Arguments.of("1.0.0-rc", false, "RC version is not stable"),
Arguments.of("1.0.0-snapshot", false, "Snapshot version is not stable"),
Arguments.of("1.0.0-milestone", false, "Milestone version is not stable"),
// Edge cases
Arguments.of(null, false, "Null version is not stable"));
}
@ParameterizedTest
@MethodSource("versionTypeTestData")
void testGetVersionType(String version, String expectedType, String description) {
// When
String versionType = versionComparator.getVersionTypeString(version);
// Then
assertThat(versionType).as(description).isEqualTo(expectedType);
}
private static Stream<Arguments> versionTypeTestData() {
return Stream.of(
// Stable versions
Arguments.of("1.0.0", "stable", "Plain numeric version is stable"),
Arguments.of("1.0.0-final", "stable", "Final qualifier is stable"),
Arguments.of("1.0.0-ga", "stable", "GA qualifier is stable"),
Arguments.of("1.0.0-release", "stable", "Release qualifier is stable"),
// Alpha versions
Arguments.of("1.0.0-alpha", "alpha", "Alpha version"),
Arguments.of("1.0.0-a1", "alpha", "Alpha with number"),
Arguments.of("1.0.0-dev", "alpha", "Dev version treated as alpha"),
Arguments.of("1.0.0-preview", "alpha", "Preview version treated as alpha"),
// Beta versions
Arguments.of("1.0.0-beta", "beta", "Beta version"),
Arguments.of("1.0.0-b1", "beta", "Beta with number"),
// RC versions
Arguments.of("1.0.0-rc", "rc", "RC version"),
Arguments.of("1.0.0-cr", "rc", "CR version treated as RC"),
Arguments.of("1.0.0-candidate", "rc", "Candidate version treated as RC"),
// Milestone versions
Arguments.of("1.0.0-milestone", "milestone", "Milestone version"),
Arguments.of("1.0.0-m1", "milestone", "Milestone with number"),
// Edge cases
Arguments.of(null, "unknown", "Null version returns unknown"),
Arguments.of("1.0.0-custom", "alpha", "Unknown qualifier defaults to alpha for safety"));
}
}
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added (Unreleased)
### Changed (Unreleased)
### Fixed (Unreleased)
### Removed (Unreleased)
## [1.5.1] - 2025-10-22
**Corporate Environment Support Release** - Adds dual-image build strategy and comprehensive documentation for corporate networks with SSL inspection/MITM proxies.
### Added (1.5.1)
- **Corporate Certificate Guide**: Complete documentation (`CORPORATE-CERTIFICATES.md`) for building custom native images with corporate SSL certificates using Paketo buildpack certificate bindings
- **Dual-Image Build Strategy**: CI/CD now builds 4 image variants (amd64/arm64, with/without Context7) to support different deployment scenarios
- **Spring Profiles for Context7 Control**:
- `application-no-context7.yaml` - Disables Context7 integration for corporate environments
- `application-docker.yaml` - Controls Spring Boot banner for clean MCP protocol compliance
- **Image Variants Documentation**: Added comprehensive table in README explaining when to use each image tag (`latest`, `latest-noc7`, `<version>`, `<version>-noc7`)
### Changed (1.5.1)
- **Build Scripts**: Updated all Unix and Windows build scripts to support `-noc7` image builds with Spring profile activation
- **README**: Added troubleshooting section for corporate SSL environments and link to certificate guide
- **CI/CD Workflow**: Enhanced to build and publish 4 multi-architecture image manifests
- **Updated Dependencies**:
- Resilience4j updated to 2.3.0 (from 2.2.0)
### Fixed (1.5.1)
- **SSL Handshake Issues**: Resolved Context7 connection failures in corporate environments with SSL inspection by providing `-noc7` image variants and custom certificate build solution
- **MCP Protocol Interference**: Fixed Spring Boot banner appearing before logback initialization by using `application-docker.yaml` profile
## [1.5.0] - 2025-10-19
**Performance & Resilience Release** - Introduces OkHttp 5 for HTTP/2 support, circuit breaker patterns, and improved reliability. Includes code quality improvements and dependency updates.
### Added (1.5.0)
- **OkHttp5 Integration**: Direct HTTP/2 support with connection pooling for improved performance and resource efficiency (5.2.1 - latest stable)
- **Resilience4j Patterns**: Added circuit breaker, retry, and rate limiter patterns to `MavenCentralService` for improved reliability
- **Connection Pool Configuration**: New `maven.central.connection-pool-size` property (default: 50) for tuning OkHttp connection pooling
- **Spring Configuration Metadata**: Added `maven.central.connection-pool-size` property documentation for IDE autocomplete support
### Changed (1.5.0)
- **HTTP Client Architecture**: Introduced OkHttp 5.2.1 as the primary HTTP client (replacing SimpleClientHttpRequestFactory) for HTTP/2 support and improved connection pooling
- **Updated Dependencies**:
- Spring Boot parent updated to 3.5.6 (from 3.5.4)
- fmt-maven-plugin updated to 2.29 (from 2.27)
- **MCP Client Transport**: Changed from SYNC SSE client to ASYNC streamable-http transport for better performance and compatibility
### Removed (1.5.0)
- **Test Utilities**: Removed unused `ClientStdio` test class (62 lines)
- **Legacy REST Configuration**: Removed direct `SimpleClientHttpRequestFactory` bean in favor of OkHttp5-backed RestClient
## [1.4.0] - 2025-08-17
**Direct Maven Repository Access Release** - All existing MCP tools and features are fully retained with significantly improved accuracy and performance by reading maven-metadata.xml files directly from Maven Central instead of using the search API.
### Added (1.4.0)
- **Maven Metadata XML Parsing:** Now reads maven-metadata.xml files directly from Maven Central repository for accurate version information
- **Jackson XML Support:** Added XML parsing capabilities for universal JVM build tool support
- **Direct Repository Tests:** Comprehensive test coverage for maven-metadata.xml access functionality
- **Improved Version Accuracy:** Eliminates search API delays and version ordering quirks
### Changed (1.4.0)
- **Data Source:** Now uses `https://repo1.maven.org/maven2` maven-metadata.xml files instead of `search.maven.org` Solr API
- **Timestamp Accuracy:** Implemented a more accurate timestamp retrieval method that fetches real timestamps for recent versions via HTTP HEAD requests.
- **Enhanced Performance:** Smaller XML metadata files provide faster response times than large JSON search results
- **Simplified Configuration:** Removed complex strategy patterns for cleaner, more maintainable codebase
- **Updated Dependencies:** Added Jackson XML module for maven-metadata.xml parsing support
### Fixed (1.4.0)
- **Version Ordering Issues:** Direct repository access provides accurate version ordering without Solr search index delays
- **Date-like Version Anomalies:** Fixed incorrect "latest" results for artifacts like commons-io with date-like versions
- **JGit Release Classification:** Service pack releases with `-r` suffix now correctly classified as stable
- **Timestamp Analysis:** Repository metadata provides authoritative version information for analytical features
- **Error Handling:** Graceful null returns for non-existent artifacts instead of exceptions for better API consistency
- **Date/Time Processing:** Improved timestamp parsing using proper Java time APIs for more accurate analytical features
### Removed (1.4.0)
- **Search API Dependency:** Eliminated reliance on `search.maven.org/solrsearch/select` for core functionality
- **Unused Models:** Removed the obsolete `MavenSearchResponse` model after refactoring.
- **Strategy Configuration:** Removed complex strategy patterns for simplified architecture
- **Legacy Properties:** Deprecated `maven.central.base-url` in favor of `maven.central.repository-base-url`
## [1.3.0] - 2025-08-09
### Added (1.3.0)
- **Type-safe ToolResponse architecture**: Unified response wrapper for all MCP tools with sealed interface pattern
- **Performance optimizations**: Early-exit algorithms with 50-80% performance improvements
- **Enhanced test coverage**: Critical version parsing test scenarios for complex pre-release versions
- **Simplified Context7 orchestration**: Clear step-by-step tool usage instructions with web search fallback
### Changed (1.3.0)
- **BREAKING**: All MCP tool methods now return `ToolResponse` instead of JSON strings for better type safety
- **Context7Guidance model**: Simplified from 5 fields to single `orchestrationInstructions` field for better clarity
- **Library name resolution**: Now uses Maven artifactId directly instead of ecosystem-specific mapping for universal coverage
- **Response format**: Consistent response structure with `ToolResponse.Success<T>` and `ToolResponse.Error`
- **User-Agent Header**: Updated to Maven-Tools-MCP/1.3.0
- **Native image configuration**: Updated reflection hints to include ToolResponse and nested records
- **Algorithm optimizations**: Implemented early-exit optimization in version type classification
- **Stream operations**: Replaced manual loops with optimized stream operations where beneficial
- **Time calculations**: Deduplicated redundant timestamp arithmetic operations
### Fixed (1.3.0)
- **Critical version parsing bug**: Fixed corruption of pre-release versions (e.g., "2.0.0-M1" was becoming "2-milestone-1")
- **Cache configuration**: Resolved type collision issues by using separate cache instances per region
- **SonarQube warnings**: Resolved string literal duplication, cognitive complexity, and primitive null comparison issues
### Removed (1.3.0)
- **Unused code**: Removed duplicate ToolResponseOperation interface and associated error handling method
- **Obsolete dependencies**: Cleaned up JsonResponseService references and inline imports
- **Context7 complexity**: Eliminated redundant guidance fields (suggestedSearch, searchHints, complexity, documentationFocus)
- **Ecosystem-specific logic**: Removed 70+ lines of hardcoded Spring/Hibernate/Jackson library mapping for maintainability
## [1.2.0] - 2025-07-24
### Added (1.2.0)
- **Guided Delegation Architecture**: Context7 guidance hints in Maven tool responses for intelligent LLM orchestration
- `Context7Properties` configuration with `context7.enabled` setting (defaults to true)
- `Context7Guidance` model with smart search suggestions and ecosystem-specific hints
- Context7 guidance integration in response models (when context7.enabled=true):
- `VersionComparisonResponse` includes migration guidance hints for updates
- `DependencyAgeResponse` includes modernization guidance for aging/stale dependencies
- `ProjectHealthAnalysis` includes upgrade guidance hints for health issues
- Raw Context7 MCP tools automatically exposed via Spring AI MCP client:
- `resolve-library-id` - Library search and resolution (when context7.enabled=true)
- `get-library-docs` - Documentation retrieval with topic queries (when context7.enabled=true)
- Enhanced tool descriptions with Context7 guidance references
- Type-safe record models: `ProjectHealthAnalysis`, `VersionsByType` replacing HashMap usage
- `JacksonConfig` with JDK8 and JSR310 module registration for serialization support
### Changed (1.2.0)
- **Simplified Architecture**: Eliminated complex internal Context7 integration (688 lines removed)
- **Tool Consolidation**: Reduced from 10 to 8 tools for better usability:
- Enhanced `get_latest_version` to replace `get_stable_version` with `preferStable` parameter
- Enhanced `check_multiple_dependencies` to replace `check_multiple_stable_versions` with `stableOnly` parameter
- **Guided Delegation**: Maven tools now provide Context7 guidance hints instead of internal documentation calls
- **Conditional Context7 Guidance**: Guidance hints only included when `context7.enabled=true` (enabled by default)
- Removed Context7 parameters from Maven tools (includeMigrationGuidance, includeUpgradeStrategy, includeModernizationGuidance)
- Context7 integration enabled by default (context7.enabled=true) with clean responses when disabled
- Updated tool descriptions to reference separate Context7 tool usage for documentation needs
### Fixed (1.2.0)
- Eliminated Context7 data quality issues through guided delegation approach
- Simplified maintenance by removing complex MCP client orchestration
- Enhanced transparency - LLMs can adapt when Context7 returns incorrect results
### Removed (1.2.0)
- **Complex Context7 Integration**: Removed 688 lines of internal Context7 integration code:
- `DocumentationEnrichmentService` (replaced with guided delegation)
- `DocumentationEnrichmentProperties` configuration
- Complex MCP client orchestration and error handling
- **Context7 Tool Parameters**: Removed from Maven tool method signatures:
- `includeMigrationGuidance` parameter
- `includeUpgradeStrategy` parameter
- `includeModernizationGuidance` parameter
- **Deprecated Tools**: Removed redundant tools to reduce cognitive overload:
- `get_stable_version` (functionality moved to `get_latest_version` with `preferStable=true`)
- `check_multiple_stable_versions` (functionality moved to `check_multiple_dependencies` with `stableOnly=true`)
## [1.1.1] - 2025-07-23
### Fixed (1.1.1)
- Native image configuration for analytical intelligence models
- Added reflection hints for all v1.1.0 analytical model classes (DependencyAgeAnalysis, ReleasePatternAnalysis, VersionTimelineAnalysis)
- Proper native image compatibility for new analytical features
## [1.1.0] - 2025-07-23
### Added (1.1.0)
- **Analytical Intelligence Tools**: Four new MCP tools for advanced dependency analysis
- `analyze_dependency_age` - Classify dependencies as fresh/current/aging/stale with actionable insights
- `analyze_release_patterns` - Analyze maintenance activity, release velocity, and predict next releases
- `get_version_timeline` - Enhanced version timeline with temporal analysis and release gap detection
- `analyze_project_health` - Comprehensive health scoring for multiple dependencies with risk assessment
- **Enhanced MavenCentralService**: Added timestamp-aware methods for age analysis (`getAllVersionsWithTimestamps`, `getRecentVersionsWithTimestamps`)
- **New Model Classes**: Added comprehensive analytical data structures (`DependencyAgeAnalysis`, `ReleasePatternAnalysis`, `VersionTimelineAnalysis`)
- **Virtual Thread Support**: Concurrent bulk analysis for improved performance
- **New Parameters**: Added analytical parameters (`maxAgeInDays`, `monthsToAnalyze`, `versionCount`)
### Changed (1.1.0)
- **Enhanced Tool Descriptions**: Updated existing tool descriptions for better clarity and universal JVM build tool support
- **Improved Documentation**: Updated README.md and CLAUDE.md with analytical intelligence examples and scope decisions
- **User-Agent Header**: Updated to Maven-Tools-MCP/1.1.0
### Performance
- **Bulk Analysis**: Added concurrent processing for multiple dependency health analysis
- **Caching**: Analytical tools leverage existing 24-hour cache infrastructure
- **Memory Optimization**: Efficient data structures for timeline and pattern analysis
## [1.0.0] - 2025-07-23
### Breaking Changes
This major release updates tool names and adds stability parameters while maintaining compatibility with all JVM build tools.
### ⚠️ BREAKING CHANGES
**Tool Renaming for Universal Appeal:**
- `maven_get_latest` → `get_latest_version`
- `maven_get_stable` → `get_stable_version`
- `maven_check_exists` → `check_version_exists`
- `maven_bulk_check_latest` → `check_multiple_dependencies`
- `maven_bulk_check_stable` → `check_multiple_stable_versions`
- `maven_compare_versions` → `compare_dependency_versions`
### Added (1.0.0)
**New Tool Parameters:**
- `preferStable` parameter for `get_latest_version` - prioritizes stable versions in comprehensive analysis
- `stableOnly` parameter for `check_multiple_dependencies` - filters to production-ready versions only
- `onlyStableTargets` parameter for `compare_dependency_versions` - only suggests stable upgrades for production safety
**JVM Build Tool Support:**
- Support for Maven, Gradle, SBT, Mill, and any JVM build tool
- Standard Maven coordinate format for all tools
- Cross-platform examples and documentation
**Stability Controls:**
- Stability preference controls across all tools
- Filtering options for production deployments
- Upgrade safety controls
### Changed (1.0.0)
**Documentation Updates:**
- Application name remains `maven-tools-mcp` for consistency
- Tool descriptions updated for JVM ecosystem support
- Multi-build tool examples and scenarios
- Examples include Kotlin, Scala, Retrofit, Spark dependencies
**README Updates:**
- Build tool support matrix
- Usage examples with stability controls
- Multi-build tool use cases
- Production deployment examples
**Technical:**
- Updated tool method names and parameters
- Version updated to 1.0.0
- Test suite updated for new signatures
## [0.1.3] - 2025-06-30
### Added (0.1.3)
- Comprehensive version info in dependency check results and improved JSON serialization
- Support for multiple builder platforms in Docker configuration (native AMD64 and ARM64 builds)
- Helpful hints and format examples in tool descriptions for better LLM guidance
- Demo GIF and improved documentation for setup and usage
### Changed (0.1.3)
- Refactored and clarified README with detailed command descriptions, examples, and improved user guidance
- Upgraded Spring Boot to 3.5.3
- Internal refactoring for maintainability and clarity
- Improved formatting and readability of tool descriptions
### Fixed (0.1.3)
- Docker build and manifest creation for multi-architecture images
- Build scripts and Docker image configuration for reliability and compatibility
- Response structure for bulk and compare tools
## [0.1.2] - 2025-06-09
### Added (0.1.2)
- Docker support for MCP server deployment with pre-built images and Docker Compose
### Changed (0.1.2)
- Enhanced build tooling and documentation for Docker deployment
- Use maven-artifact for version comparisons
## [0.1.1] - 2025-06-08
### Added (0.1.1)
- Virtual thread support for optimal I/O-bound performance in bulk operations
### Changed (0.1.1)
- Extended cache TTL from 1 hour to 24 hours for optimal performance
### Removed (0.1.1)
- Support for 'snapshot' version type in all tools, API responses, and documentation. Only stable, rc, beta, alpha, and milestone are now supported.
- Reason: The underlying Maven Central API used for dependency search does not manage or return snapshot versions, so accurate and reliable snapshot support is not possible.
- All code, documentation, and tests for the `maven_analyze_pom` tool (POM file analysis) have been removed.
- Reason: Full POM analysis is not efficient or LLM-friendly, and is out of scope for this project. Only direct dependency/version tools are supported.
## [0.1.0] - 2025-06-07
### Added (0.1.0)
- Initial release
- MCP tools for Maven dependency management:
- `maven_get_latest` - Get latest version (stable, rc, beta, alpha, milestone)
- `maven_check_exists` - Check if version exists
- `maven_get_stable` - Get latest stable version
- `maven_bulk_check_latest` - Bulk version checking
- `maven_bulk_check_stable` - Bulk stable version checking
- `maven_analyze_pom` - POM file analysis
- `maven_compare_versions` - Version comparison
- Caching with 1 hour TTL
- Version classification (stable, rc, beta, alpha, milestone)
- Works with Claude Desktop and GitHub Copilot
### Technical (0.1.0)
- Java 24, Spring Boot 3.5.4, Spring AI
- MCP Protocol 2024-11-05
- Unit and integration tests
- Maven Central API integration
[Unreleased]: https://github.com/arvindand/maven-tools-mcp/compare/v1.4.0...HEAD
[1.4.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/arvindand/maven-tools-mcp/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.3...v1.0.0
[0.1.3]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/arvindand/maven-tools-mcp/releases/tag/v0.1.0
```
--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/service/MavenDependencyTools.java:
--------------------------------------------------------------------------------
```java
package com.arvindand.mcp.maven.service;
import com.arvindand.mcp.maven.config.Context7Properties;
import com.arvindand.mcp.maven.model.BulkCheckResult;
import com.arvindand.mcp.maven.model.DependencyAge;
import com.arvindand.mcp.maven.model.DependencyAgeAnalysis;
import com.arvindand.mcp.maven.model.DependencyInfo;
import com.arvindand.mcp.maven.model.MavenArtifact;
import com.arvindand.mcp.maven.model.MavenCoordinate;
import com.arvindand.mcp.maven.model.ProjectHealthAnalysis;
import com.arvindand.mcp.maven.model.ReleasePatternAnalysis;
import com.arvindand.mcp.maven.model.StabilityFilter;
import com.arvindand.mcp.maven.model.ToolResponse;
import com.arvindand.mcp.maven.model.VersionComparison;
import com.arvindand.mcp.maven.model.VersionInfo;
import com.arvindand.mcp.maven.model.VersionInfo.VersionType;
import com.arvindand.mcp.maven.model.VersionTimelineAnalysis;
import com.arvindand.mcp.maven.model.VersionTimelineAnalysis.RecentActivity.ActivityLevel;
import com.arvindand.mcp.maven.model.VersionTimelineAnalysis.TimelineEntry.ReleaseGap;
import com.arvindand.mcp.maven.model.VersionTimelineAnalysis.VelocityTrend.TrendDirection;
import com.arvindand.mcp.maven.model.VersionsByType;
import com.arvindand.mcp.maven.util.MavenCoordinateParser;
import com.arvindand.mcp.maven.util.VersionComparator;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
/**
* Main service providing MCP tools for Maven dependency management.
*
* @author Arvind Menon
* @since 0.1.0
*/
@Service
public class MavenDependencyTools {
private static final String UNEXPECTED_ERROR = "Unexpected error";
private static final String MAVEN_CENTRAL_ERROR = "Maven Central error: ";
private static final String INVALID_MAVEN_COORDINATE_FORMAT = "Invalid Maven coordinate format: ";
private static final String SUCCESS_STATUS = "success";
private static final String ACTIVE_MAINTENANCE = "active";
// Health level constants
private static final String EXCELLENT_HEALTH = "excellent";
private static final String GOOD_HEALTH = "good";
private static final String FAIR_HEALTH = "fair";
private static final String POOR_HEALTH = "poor";
// Age classification constants
private static final String FRESH_AGE = "fresh";
private static final String CURRENT_AGE = "current";
private static final String AGING_AGE = "aging";
private static final String STALE_AGE = "stale";
private static final Logger logger = LoggerFactory.getLogger(MavenDependencyTools.class);
// Analysis constants
private static final int DEFAULT_ANALYSIS_MONTHS = 24;
private static final int DEFAULT_VERSION_COUNT = 20;
private static final int ACCURATE_TIMESTAMP_VERSION_LIMIT = 30;
private static final int RECENT_VERSIONS_LIMIT = 10;
private static final int MILLISECONDS_TO_DAYS = 1000 * 60 * 60 * 24;
private static final int DAYS_IN_MONTH = 30;
// Health scoring constants
private static final int PERFECT_HEALTH_SCORE = 100;
private static final int CURRENT_VERSION_PENALTY = 10;
private static final int AGING_VERSION_PENALTY = 30;
private static final int STALE_VERSION_PENALTY = 60;
private static final int MODERATE_MAINTENANCE_PENALTY = 15;
private static final int SLOW_MAINTENANCE_PENALTY = 40;
private static final int AGE_THRESHOLD_PENALTY = 20;
// Health classification thresholds
private static final int EXCELLENT_HEALTH_THRESHOLD = 80;
private static final int GOOD_HEALTH_THRESHOLD = 65;
private static final int FAIR_HEALTH_THRESHOLD = 50;
// Stability analysis constants
private static final int VERY_HIGH_STABILITY_THRESHOLD = 80;
private static final int LOW_STABILITY_THRESHOLD = 50;
private final MavenCentralService mavenCentralService;
private final VersionComparator versionComparator;
private final Context7Properties context7Properties;
public MavenDependencyTools(
MavenCentralService mavenCentralService,
VersionComparator versionComparator,
Context7Properties context7Properties) {
this.mavenCentralService = mavenCentralService;
this.versionComparator = versionComparator;
this.context7Properties = context7Properties;
}
/**
* Common error handling for tool operations that return data objects.
*
* @param operation the operation to execute
* @param <T> the return type
* @return ToolResponse containing either success result or error
*/
private <T> ToolResponse executeToolOperation(ToolOperation<T> operation) {
try {
T result = operation.execute();
return ToolResponse.Success.of(result);
} catch (IllegalArgumentException e) {
return ToolResponse.Error.of(INVALID_MAVEN_COORDINATE_FORMAT + e.getMessage());
} catch (MavenCentralException e) {
return ToolResponse.Error.of(MAVEN_CENTRAL_ERROR + e.getMessage());
} catch (Exception e) {
logger.error(UNEXPECTED_ERROR, e);
return ToolResponse.Error.of(UNEXPECTED_ERROR + ": " + e.getMessage());
}
}
@FunctionalInterface
private interface ToolOperation<T> {
T execute() throws IllegalArgumentException, MavenCentralException;
}
/**
* Get the latest version of any dependency from Maven Central (works with Maven, Gradle, SBT,
* Mill).
*
* @param dependency the dependency coordinate (groupId:artifactId)
* @param stabilityFilter controls version filtering: ALL (default), STABLE_ONLY, or PREFER_STABLE
* @return JSON response with latest versions by type
*/
@SuppressWarnings("java:S100") // MCP tool method naming
@Tool(
description =
"Single dependency. Returns newest versions by type (stable/rc/beta/alpha/milestone). Set"
+ " stabilityFilter to ALL (default), STABLE_ONLY, or PREFER_STABLE. Use when asked:"
+ " 'what's the latest version of X?' Works with all JVM build tools.")
public ToolResponse get_latest_version(
@ToolParam(
description =
"Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
+ " Example: 'org.springframework:spring-core'")
String dependency,
@ToolParam(
description =
"Stability filter: ALL (all versions), STABLE_ONLY (production-ready only), or"
+ " PREFER_STABLE (prioritize stable, show others too). Default:"
+ " PREFER_STABLE",
required = false)
@Nullable
StabilityFilter stabilityFilter) {
return executeToolOperation(
() -> {
MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
if (allVersions.isEmpty()) {
return notFoundResponse(coordinate);
}
StabilityFilter filter =
stabilityFilter != null ? stabilityFilter : StabilityFilter.PREFER_STABLE;
return buildVersionsByType(coordinate, allVersions, filter);
});
}
/**
* Check if specific dependency version exists and identify its stability type.
*
* @param dependency the dependency coordinate (groupId:artifactId)
* @param version the version to check
* @return JSON response with existence status and version type
*/
@SuppressWarnings("java:S100") // MCP tool method naming
@Tool(
description =
"Single dependency + version. Validates existence on Maven Central and classifies its"
+ " stability (stable/rc/beta/alpha/milestone/snapshot). Use when asked: 'does X:Y"
+ " exist?' or 'is version V stable?'")
public ToolResponse check_version_exists(
@ToolParam(
description =
"Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
+ " Example: 'org.springframework:spring-core'")
String dependency,
@ToolParam(
description =
"Specific version string to check for existence. Example: '6.1.4' or"
+ " '2.7.18-SNAPSHOT'")
String version) {
return executeToolOperation(
() -> {
MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
String versionToCheck = coordinate.version() != null ? coordinate.version() : version;
if (versionToCheck == null || versionToCheck.trim().isEmpty()) {
throw new IllegalArgumentException(
"Version must be provided either in dependency string or version parameter");
}
boolean exists = mavenCentralService.checkVersionExists(coordinate, versionToCheck);
String versionType = versionComparator.getVersionTypeString(versionToCheck);
return DependencyInfo.success(
coordinate,
versionToCheck,
exists,
versionType,
versionComparator.isStableVersion(versionToCheck),
null);
});
}
/**
* Check latest versions for multiple dependencies with filtering options.
*
* @param dependencies comma or newline separated list of dependency coordinates
* @param stabilityFilter controls version filtering: ALL, STABLE_ONLY, or PREFER_STABLE
* @return JSON response with bulk check results
*/
@SuppressWarnings("java:S100") // MCP tool method naming
@Tool(
description =
"Bulk. For many coordinates (no versions), returns per-dependency latest versions by"
+ " type. Set stabilityFilter to ALL (default), STABLE_ONLY, or PREFER_STABLE."
+ " Use for audits of multiple dependencies.")
public ToolResponse check_multiple_dependencies(
@ToolParam(
description =
"Comma or newline separated list of Maven dependency coordinates in format"
+ " 'groupId:artifactId' (NO versions). Example:"
+ " 'org.springframework:spring-core,junit:junit'")
String dependencies,
@ToolParam(
description =
"Stability filter: ALL (all versions), STABLE_ONLY (production-ready only), or"
+ " PREFER_STABLE (prioritize stable). Default: ALL",
required = false)
@Nullable
StabilityFilter stabilityFilter) {
return executeToolOperation(
() -> {
List<String> depList = parseDependencies(dependencies);
StabilityFilter filter = stabilityFilter != null ? stabilityFilter : StabilityFilter.ALL;
List<BulkCheckResult> results;
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<CompletableFuture<BulkCheckResult>> futures =
depList.stream()
.distinct()
.map(
dep ->
CompletableFuture.supplyAsync(
() -> processVersionCheck(dep, filter), executor))
.toList();
results = futures.stream().map(CompletableFuture::join).toList();
}
return results;
});
}
/**
* Compare current dependency versions with latest available and show upgrade recommendations.
*
* @param currentDependencies comma or newline separated list of dependency coordinates with
* versions
* @param stabilityFilter controls upgrade targets: ALL, STABLE_ONLY, or PREFER_STABLE
* @return JSON response with version comparison and update recommendations
*/
@SuppressWarnings("java:S100") // MCP tool method naming
@Tool(
description =
"Bulk compare. Input includes versions. Suggests upgrades and classifies update type"
+ " (major/minor/patch). Set stabilityFilter to ALL (default), STABLE_ONLY, or"
+ " PREFER_STABLE. Never suggests downgrades.")
public ToolResponse compare_dependency_versions(
@ToolParam(
description =
"Comma or newline separated list of dependency coordinates WITH versions in"
+ " format 'groupId:artifactId:version'. Example:"
+ " 'org.springframework:spring-core:6.0.0,junit:junit:4.12'")
String currentDependencies,
@ToolParam(
description =
"Stability filter: ALL (any version), STABLE_ONLY (production-ready only), or"
+ " PREFER_STABLE (prioritize stable). Default: ALL",
required = false)
@Nullable
StabilityFilter stabilityFilter) {
return executeToolOperation(
() -> {
List<String> depList = parseDependencies(currentDependencies);
StabilityFilter filter = stabilityFilter != null ? stabilityFilter : StabilityFilter.ALL;
List<VersionComparison.DependencyComparisonResult> results;
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<CompletableFuture<VersionComparison.DependencyComparisonResult>> futures =
depList.stream()
.distinct()
.map(
dep ->
CompletableFuture.supplyAsync(
() -> compareDependencyVersion(dep, filter), executor))
.toList();
results = futures.stream().map(CompletableFuture::join).toList();
}
VersionComparison.UpdateSummary summary = calculateUpdateSummary(results);
return new VersionComparison(Instant.now(), results, summary);
});
}
/**
* Analyze dependency age and freshness classification with actionable insights.
*
* @param dependency the dependency coordinate (groupId:artifactId)
* @param maxAgeInDays optional maximum acceptable age in days (default: no limit)
* @return JSON response with age analysis and recommendations
*/
@SuppressWarnings("java:S100") // MCP tool method naming
@Tool(
description =
"Single dependency. Returns days since last release and freshness"
+ " (fresh/current/aging/stale), with actionable insights. Use when asked about 'how"
+ " old' or 'last release' of a library.")
public ToolResponse analyze_dependency_age(
@ToolParam(
description =
"Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
+ " Example: 'org.springframework:spring-core'")
String dependency,
@ToolParam(
description =
"Optional maximum acceptable age threshold in days. If specified and dependency"
+ " exceeds this age, additional recommendations are provided. No limit if"
+ " not specified",
required = false)
@Nullable
Integer maxAgeInDays) {
return executeToolOperation(
() -> {
MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
List<MavenArtifact> versions =
mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, 1);
if (versions.isEmpty()) {
return notFoundResponse(coordinate);
}
MavenArtifact latestVersion = versions.get(0);
DependencyAgeAnalysis basicAnalysis =
DependencyAgeAnalysis.fromTimestamp(
coordinate.toCoordinateString(),
latestVersion.version(),
latestVersion.timestamp());
// Add custom recommendation if maxAgeInDays is specified
DependencyAgeAnalysis analysis = basicAnalysis;
if (maxAgeInDays != null && basicAnalysis.daysSinceLastRelease() > maxAgeInDays) {
analysis =
new DependencyAgeAnalysis(
basicAnalysis.dependency(),
basicAnalysis.latestVersion(),
basicAnalysis.ageClassification(),
basicAnalysis.daysSinceLastRelease(),
basicAnalysis.lastReleaseDate(),
basicAnalysis.ageDescription(),
"Exceeds specified age threshold of "
+ maxAgeInDays
+ " days - "
+ basicAnalysis.recommendation());
}
// Create response with basic analysis
return DependencyAge.from(analysis, context7Properties.enabled());
});
}
/**
* Analyze release patterns and maintenance activity to predict future releases.
*
* @param dependency the dependency coordinate (groupId:artifactId)
* @param monthsToAnalyze number of months of history to analyze (default: 24)
* @return JSON response with release pattern analysis and predictions
*/
@SuppressWarnings("java:S100") // MCP tool method naming
@Tool(
description =
"Single dependency. Analyzes historical releases to infer cadence, consistency, and"
+ " likely next-release timeframe. Useful for maintenance and planning.")
public ToolResponse analyze_release_patterns(
@ToolParam(
description =
"Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
+ " Example: 'com.fasterxml.jackson.core:jackson-core'")
String dependency,
@ToolParam(
description =
"Number of months of historical release data to analyze for patterns and"
+ " predictions. Default is 24 months if not specified",
required = false)
@Nullable
Integer monthsToAnalyze) {
return executeToolOperation(
() -> {
MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
int analysisMonths = monthsToAnalyze != null ? monthsToAnalyze : DEFAULT_ANALYSIS_MONTHS;
List<MavenArtifact> allVersions =
mavenCentralService.getRecentVersionsWithAccurateTimestamps(
coordinate, ACCURATE_TIMESTAMP_VERSION_LIMIT);
if (allVersions.isEmpty()) {
throw new MavenCentralException(
"No versions found for " + coordinate.toCoordinateString());
}
return analyzeReleasePattern(
coordinate.toCoordinateString(), allVersions, analysisMonths);
});
}
/**
* Get enhanced version timeline with temporal analysis and release patterns.
*
* @param dependency the dependency coordinate (groupId:artifactId)
* @param versionCount number of recent versions to include (default: 20)
* @return JSON response with version timeline and temporal insights
*/
@SuppressWarnings("java:S100") // MCP tool method naming
@Tool(
description =
"Single dependency. Returns a timeline of recent versions with dates, gaps, and stability"
+ " patterns. Use for quick release history snapshots.")
public ToolResponse get_version_timeline(
@ToolParam(
description =
"Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
+ " Example: 'org.junit.jupiter:junit-jupiter'")
String dependency,
@ToolParam(
description =
"Number of recent versions to include in timeline analysis. Default is 20"
+ " versions if not specified. Typical range: 10-50",
required = false)
@Nullable
Integer versionCount) {
return executeToolOperation(
() -> {
MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
int maxVersions = versionCount != null ? versionCount : DEFAULT_VERSION_COUNT;
List<MavenArtifact> versions =
mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, maxVersions);
if (versions.isEmpty()) {
throw new MavenCentralException(
"No versions found for " + coordinate.toCoordinateString());
}
return analyzeVersionTimeline(coordinate.toCoordinateString(), versions);
});
}
/**
* Analyze overall health of multiple dependencies with age and maintenance insights.
*
* @param dependencies comma or newline separated list of dependency coordinates
* @param maxAgeInDays optional maximum acceptable age in days for health scoring
* @param stabilityFilter controls recommendations: ALL, STABLE_ONLY, or PREFER_STABLE
* @return JSON response with project health summary and individual dependency analysis
*/
@SuppressWarnings("java:S100") // MCP tool method naming
@Tool(
description =
"Bulk project view. Summarizes health across many dependencies using age and maintenance"
+ " patterns, with concise recommendations. Set stabilityFilter to ALL (default),"
+ " STABLE_ONLY, or PREFER_STABLE for upgrade recommendations.")
public ToolResponse analyze_project_health(
@ToolParam(
description =
"Comma or newline separated list of Maven dependency coordinates in format"
+ " 'groupId:artifactId' (NO versions). Example:"
+ " 'org.springframework:spring-core,junit:junit'")
String dependencies,
@ToolParam(
description =
"Optional maximum acceptable age threshold in days for health scoring."
+ " Dependencies exceeding this age receive lower health scores. No age"
+ " penalty if not specified",
required = false)
@Nullable
Integer maxAgeInDays,
@ToolParam(
description =
"Stability filter: ALL (any version), STABLE_ONLY (production-ready only), or"
+ " PREFER_STABLE (prioritize stable). Default: PREFER_STABLE",
required = false)
@Nullable
StabilityFilter stabilityFilter) {
return executeToolOperation(
() -> {
List<String> depList = parseDependencies(dependencies);
if (depList.isEmpty()) {
throw new IllegalArgumentException("No dependencies provided for analysis");
}
// Analyze each dependency for age and patterns
List<ProjectHealthAnalysis.DependencyHealthAnalysis> dependencyAnalyses;
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<CompletableFuture<ProjectHealthAnalysis.DependencyHealthAnalysis>> futures =
depList.stream()
.distinct()
.map(
dep ->
CompletableFuture.supplyAsync(
() -> analyzeSimpleDependencyHealth(dep, maxAgeInDays), executor))
.toList();
dependencyAnalyses = futures.stream().map(CompletableFuture::join).toList();
}
return buildSimpleHealthSummary(dependencyAnalyses, maxAgeInDays);
});
}
private ToolResponse notFoundResponse(MavenCoordinate coordinate) {
String message =
"No Maven dependency found for %s:%s%s"
.formatted(
coordinate.groupId(),
coordinate.artifactId(),
coordinate.packaging() != null ? ":" + coordinate.packaging() : "");
return ToolResponse.Error.notFound(message);
}
@SuppressWarnings("java:S1172") // preferStable is used by VersionsByType.getPreferredVersion()
private VersionsByType buildVersionsByType(
MavenCoordinate coordinate, List<String> allVersions, StabilityFilter stabilityFilter) {
Map<VersionType, String> versionsByType = HashMap.newHashMap(5);
for (String version : allVersions) {
VersionType type = versionComparator.getVersionType(version);
versionsByType.putIfAbsent(type, version);
if (versionsByType.size() == 5) break;
}
return VersionsByType.create(
coordinate.toCoordinateString(),
createVersionInfo(versionsByType.get(VersionType.STABLE)),
createVersionInfo(versionsByType.get(VersionType.RC)),
createVersionInfo(versionsByType.get(VersionType.BETA)),
createVersionInfo(versionsByType.get(VersionType.ALPHA)),
createVersionInfo(versionsByType.get(VersionType.MILESTONE)),
allVersions.size());
}
private Optional<VersionInfo> createVersionInfo(String version) {
return version != null
? Optional.of(new VersionInfo(version, versionComparator.getVersionType(version)))
: Optional.empty();
}
private List<String> parseDependencies(String dependencies) {
if (dependencies == null || dependencies.trim().isEmpty()) {
return List.of();
}
return dependencies
.lines()
.flatMap(line -> Arrays.stream(line.split(",")))
.map(String::trim)
.filter(dep -> !dep.isEmpty())
.toList();
}
private BulkCheckResult processVersionCheck(String dep, StabilityFilter filter) {
return switch (filter) {
case STABLE_ONLY -> processStableVersionCheck(dep);
case ALL, PREFER_STABLE -> processComprehensiveVersionCheck(dep);
};
}
private BulkCheckResult processStableVersionCheck(String dep) {
try {
MavenCoordinate coordinate = MavenCoordinateParser.parse(dep);
List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
if (allVersions.isEmpty()) {
return BulkCheckResult.notFound(coordinate.toCoordinateString());
}
List<String> stableVersions =
allVersions.stream().filter(versionComparator::isStableVersion).toList();
String latestStable = stableVersions.isEmpty() ? null : stableVersions.get(0);
return latestStable != null
? BulkCheckResult.foundStable(
coordinate.toCoordinateString(),
latestStable,
VersionType.STABLE.getDisplayName(),
allVersions.size(),
stableVersions.size())
: BulkCheckResult.noStableVersion(coordinate.toCoordinateString(), allVersions.size());
} catch (Exception e) {
return BulkCheckResult.error(dep, e.getMessage());
}
}
private VersionComparison.DependencyComparisonResult compareDependencyVersion(
String dep, StabilityFilter stabilityFilter) {
try {
MavenCoordinate coordinate = MavenCoordinateParser.parse(dep);
String currentVersion = coordinate.version();
String latestVersion =
stabilityFilter == StabilityFilter.STABLE_ONLY
? getLatestStableVersion(coordinate)
: mavenCentralService.getLatestVersion(coordinate);
if (latestVersion == null) {
return VersionComparison.DependencyComparisonResult.notFound(
coordinate.toCoordinateString(), currentVersion);
}
if (currentVersion == null) {
return VersionComparison.DependencyComparisonResult.noCurrentVersion(
coordinate.toCoordinateString());
}
String latestType = versionComparator.getVersionTypeString(latestVersion);
String updateType = versionComparator.determineUpdateType(currentVersion, latestVersion);
boolean updateAvailable = versionComparator.compare(currentVersion, latestVersion) < 0;
// Return basic comparison result
return VersionComparison.DependencyComparisonResult.success(
coordinate.toCoordinateString(),
currentVersion,
latestVersion,
latestType,
updateType,
updateAvailable,
context7Properties.enabled());
} catch (Exception e) {
return VersionComparison.DependencyComparisonResult.error(dep, e.getMessage());
}
}
private BulkCheckResult processComprehensiveVersionCheck(String dep) {
try {
MavenCoordinate coordinate = MavenCoordinateParser.parse(dep);
List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
if (allVersions.isEmpty()) {
return BulkCheckResult.notFound(dep);
}
Map<VersionType, String> versionsByType = buildVersionsByType(allVersions);
VersionInfoCollection versionInfos = createVersionInfoCollection(versionsByType);
int stableCount = countStableVersions(allVersions);
String primaryVersion =
versionInfos.latestStable() != null
? versionInfos.latestStable().version()
: allVersions.get(0);
String primaryType =
versionInfos.latestStable() != null
? VersionType.STABLE.getDisplayName()
: versionComparator.getVersionTypeString(allVersions.get(0));
return BulkCheckResult.foundComprehensive(
dep,
primaryVersion,
primaryType,
allVersions.size(),
stableCount,
versionInfos.latestStable(),
versionInfos.latestRc(),
versionInfos.latestBeta(),
versionInfos.latestAlpha(),
versionInfos.latestMilestone());
} catch (Exception e) {
logger.error("Error processing comprehensive version check for {}: {}", dep, e.getMessage());
return BulkCheckResult.error(dep, e.getMessage());
}
}
private Map<VersionType, String> buildVersionsByType(List<String> allVersions) {
Map<VersionType, String> versionsByType = HashMap.newHashMap(5);
Set<VersionType> remainingTypes = EnumSet.allOf(VersionType.class);
for (String version : allVersions) {
VersionType type = versionComparator.getVersionType(version);
if (remainingTypes.contains(type)) {
versionsByType.putIfAbsent(type, version);
remainingTypes.remove(type);
if (remainingTypes.isEmpty()) break;
}
}
return versionsByType;
}
private VersionInfoCollection createVersionInfoCollection(
Map<VersionType, String> versionsByType) {
return new VersionInfoCollection(
createVersionInfo(versionsByType, VersionType.STABLE),
createVersionInfo(versionsByType, VersionType.RC),
createVersionInfo(versionsByType, VersionType.BETA),
createVersionInfo(versionsByType, VersionType.ALPHA),
createVersionInfo(versionsByType, VersionType.MILESTONE));
}
private VersionInfo createVersionInfo(Map<VersionType, String> versionsByType, VersionType type) {
return versionsByType.containsKey(type)
? new VersionInfo(versionsByType.get(type), type)
: null;
}
private int countStableVersions(List<String> allVersions) {
return allVersions.stream()
.mapToInt(v -> versionComparator.getVersionType(v) == VersionType.STABLE ? 1 : 0)
.sum();
}
private record VersionInfoCollection(
VersionInfo latestStable,
VersionInfo latestRc,
VersionInfo latestBeta,
VersionInfo latestAlpha,
VersionInfo latestMilestone) {}
private VersionComparison.UpdateSummary calculateUpdateSummary(
List<VersionComparison.DependencyComparisonResult> results) {
Map<String, Long> counts =
results.stream()
.filter(result -> SUCCESS_STATUS.equals(result.status()))
.collect(
Collectors.groupingBy(
VersionComparison.DependencyComparisonResult::updateType,
Collectors.counting()));
return new VersionComparison.UpdateSummary(
counts.getOrDefault("major", 0L).intValue(),
counts.getOrDefault("minor", 0L).intValue(),
counts.getOrDefault("patch", 0L).intValue(),
counts.getOrDefault("none", 0L).intValue());
}
private String getLatestStableVersion(MavenCoordinate coordinate) throws MavenCentralException {
List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
List<String> stableVersions =
allVersions.stream().filter(versionComparator::isStableVersion).toList();
return stableVersions.isEmpty() ? null : stableVersions.get(0);
}
private ReleasePatternAnalysis analyzeReleasePattern(
String dependency, List<MavenArtifact> allVersions, int analysisMonths) {
Instant now = Instant.now();
Instant cutoffDate = now.minus((long) analysisMonths * DAYS_IN_MONTH, ChronoUnit.DAYS);
// Filter versions within analysis period
List<MavenArtifact> analysisVersions =
allVersions.stream()
.filter(
v -> {
Instant releaseDate = Instant.ofEpochMilli(v.timestamp());
return releaseDate.isAfter(cutoffDate);
})
.toList();
if (analysisVersions.isEmpty()) {
// Fallback to all versions if none in analysis period
analysisVersions = allVersions.stream().limit(RECENT_VERSIONS_LIMIT).toList();
}
// Calculate release intervals
List<Long> intervals = new ArrayList<>();
for (int i = 1; i < analysisVersions.size(); i++) {
long prevTimestamp = analysisVersions.get(i).timestamp();
long currentTimestamp = analysisVersions.get(i - 1).timestamp();
long intervalDays = (currentTimestamp - prevTimestamp) / MILLISECONDS_TO_DAYS;
if (intervalDays > 0) intervals.add(intervalDays);
}
// Calculate statistics in single pass
double averageDays;
long maxInterval;
long minInterval;
if (intervals.isEmpty()) {
averageDays = 0;
maxInterval = 0;
minInterval = 0;
} else {
var stats = intervals.stream().mapToLong(Long::longValue).summaryStatistics();
averageDays = stats.getAverage();
maxInterval = stats.getMax();
minInterval = stats.getMin();
}
double releaseVelocity = averageDays > 0 ? (DAYS_IN_MONTH / averageDays) : 0;
Instant lastReleaseDate = Instant.ofEpochMilli(analysisVersions.get(0).timestamp());
long daysSinceLastRelease = Duration.between(lastReleaseDate, now).toDays();
// Classifications
ReleasePatternAnalysis.MaintenanceLevel maintenanceLevel =
ReleasePatternAnalysis.MaintenanceLevel.classify(releaseVelocity, daysSinceLastRelease);
ReleasePatternAnalysis.ReleaseConsistency consistency =
ReleasePatternAnalysis.ReleaseConsistency.classify(averageDays, maxInterval, minInterval);
// Build recent releases info
List<ReleasePatternAnalysis.ReleaseInfo> recentReleases =
analysisVersions.stream()
.limit(10)
.map(
v -> {
Instant releaseDate = Instant.ofEpochMilli(v.timestamp());
return new ReleasePatternAnalysis.ReleaseInfo(v.version(), releaseDate, null);
})
.toList();
String nextReleasePrediction =
ReleasePatternAnalysis.predictNextRelease(averageDays, daysSinceLastRelease, consistency);
String recommendation = ReleasePatternAnalysis.generateRecommendation(maintenanceLevel);
return new ReleasePatternAnalysis(
dependency,
analysisVersions.size(),
analysisMonths,
averageDays,
releaseVelocity,
maintenanceLevel,
consistency,
lastReleaseDate,
nextReleasePrediction,
recentReleases,
recommendation);
}
private VersionTimelineAnalysis analyzeVersionTimeline(
String dependency, List<MavenArtifact> versions) {
Instant now = Instant.now();
// Pre-calculate all intervals and average - single pass optimization
long[] intervalDays = new long[versions.size()];
List<Long> positiveIntervals = new ArrayList<>();
for (int i = 1; i < versions.size(); i++) {
long currentTimestamp = versions.get(i - 1).timestamp();
long prevTimestamp = versions.get(i).timestamp();
long interval = (currentTimestamp - prevTimestamp) / MILLISECONDS_TO_DAYS;
intervalDays[i] = interval;
if (interval > 0) positiveIntervals.add(interval);
}
double averageInterval =
positiveIntervals.isEmpty()
? 0
: positiveIntervals.stream().mapToLong(Long::longValue).average().orElse(0);
// Build timeline entries using pre-calculated intervals
List<VersionTimelineAnalysis.TimelineEntry> timeline = new ArrayList<>();
for (int i = 0; i < versions.size(); i++) {
MavenArtifact version = versions.get(i);
Instant releaseDate = Instant.ofEpochMilli(version.timestamp());
String relativeTime = VersionTimelineAnalysis.formatRelativeTime(releaseDate, now);
VersionType versionType = versionComparator.getVersionType(version.version());
Long daysSincePrevious = i > 0 ? intervalDays[i] : null;
ReleaseGap gap =
i > 0 ? ReleaseGap.classify(intervalDays[i], averageInterval) : ReleaseGap.NORMAL;
boolean isBreakingChange =
versionComparator.determineUpdateType("0.0.0", version.version()).equals("major");
timeline.add(
new VersionTimelineAnalysis.TimelineEntry(
version.version(),
versionType,
releaseDate,
relativeTime,
daysSincePrevious,
isBreakingChange,
gap));
}
// Calculate metrics
Instant oldestDate = Instant.ofEpochMilli(versions.get(versions.size() - 1).timestamp());
int timeSpanMonths = (int) Duration.between(oldestDate, now).toDays() / DAYS_IN_MONTH;
// Count recent activity with optimized stream operations
Instant oneMonthAgo = now.minus(DAYS_IN_MONTH, ChronoUnit.DAYS);
Instant threeMonthsAgo = now.minus(3L * DAYS_IN_MONTH, ChronoUnit.DAYS);
long releasesLastMonth =
versions.stream()
.filter(v -> Instant.ofEpochMilli(v.timestamp()).isAfter(oneMonthAgo))
.count();
long releasesLastQuarter =
versions.stream()
.filter(v -> Instant.ofEpochMilli(v.timestamp()).isAfter(threeMonthsAgo))
.count();
// Create analysis objects
VersionTimelineAnalysis.VelocityTrend velocityTrend =
new VersionTimelineAnalysis.VelocityTrend(
TrendDirection.STABLE,
"Release velocity appears stable",
releasesLastQuarter / 3.0,
versions.size() / Math.max(timeSpanMonths, 1.0),
0.0);
long stableCount =
timeline.stream().mapToLong(t -> t.versionType() == VersionType.STABLE ? 1 : 0).sum();
double stablePercentage = (double) stableCount / timeline.size() * 100;
VersionTimelineAnalysis.StabilityPattern stabilityPattern =
new VersionTimelineAnalysis.StabilityPattern(
stablePercentage,
"Mix of stable and pre-release versions",
"Regular stable releases",
stablePercentage > 70
? "Good stability pattern - safe for production use"
: "Consider waiting for stable releases");
long lastReleaseAge = Duration.between(timeline.get(0).releaseDate(), now).toDays();
VersionTimelineAnalysis.RecentActivity recentActivity =
new VersionTimelineAnalysis.RecentActivity(
(int) releasesLastMonth,
(int) releasesLastQuarter,
ActivityLevel.classify((int) releasesLastMonth, (int) releasesLastQuarter),
lastReleaseAge,
"Recent activity: " + releasesLastQuarter + " releases in last quarter");
List<String> insights = generateTimelineInsights(timeline, recentActivity, stabilityPattern);
return new VersionTimelineAnalysis(
dependency,
versions.size(),
timeline.size(),
timeSpanMonths,
timeline,
velocityTrend,
stabilityPattern,
recentActivity,
insights);
}
private List<String> generateTimelineInsights(
List<VersionTimelineAnalysis.TimelineEntry> timeline,
VersionTimelineAnalysis.RecentActivity recentActivity,
VersionTimelineAnalysis.StabilityPattern stabilityPattern) {
List<String> insights = new ArrayList<>();
if (recentActivity.activityLevel() == ActivityLevel.VERY_ACTIVE) {
insights.add("High release frequency indicates active development");
} else if (recentActivity.activityLevel() == ActivityLevel.DORMANT) {
insights.add("No recent releases - consider checking project status");
}
if (stabilityPattern.stablePercentage() > VERY_HIGH_STABILITY_THRESHOLD) {
insights.add("Strong preference for stable releases - good for production");
} else if (stabilityPattern.stablePercentage() < LOW_STABILITY_THRESHOLD) {
insights.add("Many pre-release versions - early-stage or experimental project");
}
long majorGaps =
timeline.stream().mapToLong(t -> t.releaseGap() == ReleaseGap.MAJOR_GAP ? 1 : 0).sum();
if (majorGaps > 0) {
insights.add("Found " + majorGaps + " significant gaps in release schedule");
}
return insights;
}
private ProjectHealthAnalysis.DependencyHealthAnalysis analyzeSimpleDependencyHealth(
String dependency, Integer maxAgeInDays) {
try {
MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
List<MavenArtifact> versions =
mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, 10);
if (versions.isEmpty()) {
return ProjectHealthAnalysis.DependencyHealthAnalysis.notFound(dependency);
}
MavenArtifact latestVersion = versions.get(0);
DependencyAgeAnalysis ageAnalysis =
DependencyAgeAnalysis.fromTimestamp(
coordinate.toCoordinateString(), latestVersion.version(), latestVersion.timestamp());
// Simple maintenance assessment based on recent versions
Instant now = Instant.now();
Instant sixMonthsAgo = now.minus(6L * DAYS_IN_MONTH, ChronoUnit.DAYS);
long recentVersions =
versions.stream()
.filter(v -> Instant.ofEpochMilli(v.timestamp()).isAfter(sixMonthsAgo))
.count();
String maintenanceStatus;
if (recentVersions >= 3) {
maintenanceStatus = ACTIVE_MAINTENANCE;
} else if (recentVersions >= 1) {
maintenanceStatus = "moderate";
} else {
maintenanceStatus = "slow";
}
// Health score (0-100)
int healthScore = calculateSimpleHealthScore(ageAnalysis, maintenanceStatus, maxAgeInDays);
// No upgrade strategy in basic analysis
return ProjectHealthAnalysis.DependencyHealthAnalysis.success(
dependency,
latestVersion.version(),
ageAnalysis.ageClassification().getName(),
ageAnalysis.daysSinceLastRelease(),
healthScore,
maintenanceStatus,
context7Properties.enabled());
} catch (Exception e) {
return ProjectHealthAnalysis.DependencyHealthAnalysis.error(dependency, e.getMessage());
}
}
private int calculateSimpleHealthScore(
DependencyAgeAnalysis ageAnalysis, String maintenanceStatus, Integer maxAgeInDays) {
int score = PERFECT_HEALTH_SCORE;
// Age penalty
switch (ageAnalysis.ageClassification()) {
case FRESH -> score -= 0; // No penalty
case CURRENT -> score -= CURRENT_VERSION_PENALTY;
case AGING -> score -= AGING_VERSION_PENALTY;
case STALE -> score -= STALE_VERSION_PENALTY;
}
// Maintenance penalty
switch (maintenanceStatus) {
case ACTIVE_MAINTENANCE -> score -= 0; // No penalty
case "moderate" -> score -= MODERATE_MAINTENANCE_PENALTY;
case "slow" -> score -= SLOW_MAINTENANCE_PENALTY;
default -> {
// Unknown maintenance status - no penalty
}
}
// Custom age threshold penalty
if (maxAgeInDays != null && ageAnalysis.daysSinceLastRelease() > maxAgeInDays) {
score -= AGE_THRESHOLD_PENALTY;
}
return Math.max(0, score);
}
private ProjectHealthAnalysis buildSimpleHealthSummary(
List<ProjectHealthAnalysis.DependencyHealthAnalysis> dependencyAnalyses,
Integer maxAgeInDays) {
int totalDependencies = dependencyAnalyses.size();
int successfulAnalyses =
(int)
dependencyAnalyses.stream()
.mapToLong(dep -> SUCCESS_STATUS.equals(dep.status()) ? 1 : 0)
.sum();
// Calculate averages and counts in single pass
List<ProjectHealthAnalysis.DependencyHealthAnalysis> successfulDeps =
dependencyAnalyses.stream().filter(dep -> SUCCESS_STATUS.equals(dep.status())).toList();
if (successfulDeps.isEmpty()) {
return new ProjectHealthAnalysis(
"unknown",
0,
totalDependencies,
0,
new ProjectHealthAnalysis.AgeDistribution(0, 0, 0, 0),
dependencyAnalyses,
List.of("Unable to analyze any dependencies"));
}
// Single pass through successful dependencies
DependencyMetrics metrics = calculateDependencyMetrics(successfulDeps);
double averageHealthScore = metrics.totalHealthScore() / (double) successfulDeps.size();
String overallHealth = determineOverallHealth(averageHealthScore);
// Create age distribution
ProjectHealthAnalysis.AgeDistribution ageDistribution =
new ProjectHealthAnalysis.AgeDistribution(
(int) metrics.freshCount(),
(int) metrics.currentCount(),
(int) metrics.agingCount(),
(int) metrics.staleCount());
// Generate recommendations
List<String> recommendations =
generateHealthRecommendations(metrics, successfulAnalyses, successfulDeps, maxAgeInDays);
return new ProjectHealthAnalysis(
overallHealth,
(int) Math.round(averageHealthScore),
totalDependencies,
successfulAnalyses,
ageDistribution,
dependencyAnalyses,
recommendations);
}
private DependencyMetrics calculateDependencyMetrics(
List<ProjectHealthAnalysis.DependencyHealthAnalysis> successfulDeps) {
// Calculate all metrics in optimized stream operations
int totalHealthScore =
successfulDeps.stream()
.mapToInt(ProjectHealthAnalysis.DependencyHealthAnalysis::healthScore)
.sum();
Map<String, Long> ageCounts =
successfulDeps.stream()
.collect(
Collectors.groupingBy(
ProjectHealthAnalysis.DependencyHealthAnalysis::ageClassification,
Collectors.counting()));
long activeMaintenanceCount =
successfulDeps.stream()
.filter(dep -> ACTIVE_MAINTENANCE.equals(dep.maintenanceLevel()))
.count();
return new DependencyMetrics(
totalHealthScore,
ageCounts.getOrDefault(FRESH_AGE, 0L),
ageCounts.getOrDefault(CURRENT_AGE, 0L),
ageCounts.getOrDefault(AGING_AGE, 0L),
ageCounts.getOrDefault(STALE_AGE, 0L),
activeMaintenanceCount);
}
private String determineOverallHealth(double averageHealthScore) {
if (averageHealthScore >= EXCELLENT_HEALTH_THRESHOLD) {
return EXCELLENT_HEALTH;
} else if (averageHealthScore >= GOOD_HEALTH_THRESHOLD) {
return GOOD_HEALTH;
} else if (averageHealthScore >= FAIR_HEALTH_THRESHOLD) {
return FAIR_HEALTH;
} else {
return POOR_HEALTH;
}
}
private List<String> generateHealthRecommendations(
DependencyMetrics metrics,
int successfulAnalyses,
List<ProjectHealthAnalysis.DependencyHealthAnalysis> successfulDeps,
Integer maxAgeInDays) {
List<String> recommendations = new ArrayList<>();
if (metrics.staleCount() > 0) {
recommendations.add(
"Review " + metrics.staleCount() + " stale dependencies for alternatives");
}
if (metrics.agingCount() > successfulAnalyses / 2) {
recommendations.add("Consider updating aging dependencies");
}
if (metrics.activeMaintenanceCount() < successfulAnalyses / 2) {
recommendations.add("Monitor maintenance activity for slower-updated dependencies");
}
// Add age-specific recommendations when custom threshold is set
if (maxAgeInDays != null) {
long exceedsThreshold =
successfulDeps.stream()
.filter(
dep ->
STALE_AGE.equals(dep.ageClassification())
|| AGING_AGE.equals(dep.ageClassification()))
.count();
if (exceedsThreshold > 0) {
recommendations.add(
"Found "
+ exceedsThreshold
+ " dependencies exceeding your "
+ maxAgeInDays
+ "-day age threshold");
}
}
return recommendations;
}
private record DependencyMetrics(
int totalHealthScore,
long freshCount,
long currentCount,
long agingCount,
long staleCount,
long activeMaintenanceCount) {}
}
```