diff --git a/cli/src/main/java/com/devonfw/tools/ide/github/GithubRelease.java b/cli/src/main/java/com/devonfw/tools/ide/github/GithubRelease.java index 7e2afcd09f..24b36e9a77 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/github/GithubRelease.java +++ b/cli/src/main/java/com/devonfw/tools/ide/github/GithubRelease.java @@ -6,8 +6,17 @@ * JSON data object for a github release. * * @param name the official name of the release as its version. + * @param prerelease whether this is a pre-release version. */ -public record GithubRelease(String name) implements JsonVersionItem { +public record GithubRelease(String name, boolean prerelease) implements JsonVersionItem { + + /** + * Constructor for backwards compatibility with name only (not a prerelease). + * @param name the official name of the release as its version. + */ + public GithubRelease(String name) { + this(name, false); + } @Override public String version() { diff --git a/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdater.java b/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdater.java new file mode 100644 index 0000000000..aa8241ddfc --- /dev/null +++ b/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdater.java @@ -0,0 +1,81 @@ +package com.devonfw.tools.ide.url.tool.mvnd; + +import com.devonfw.tools.ide.url.model.folder.UrlVersion; +import com.devonfw.tools.ide.url.updater.GithubUrlReleaseUpdater; +import com.devonfw.tools.ide.version.VersionComparisonResult; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * {@link GithubUrlReleaseUpdater} for Maven Daemon (mvnd). Supports both stable releases (1.x) and pre-releases (2.x) to allow users to experiment with the + * latest versions. + */ +public class MvndUrlUpdater extends GithubUrlReleaseUpdater { + + private static final VersionIdentifier MIN_MVND_VID = VersionIdentifier.of("1.0.2"); + + @Override + public String getTool() { + + return "mvnd"; + } + + @Override + protected String getGithubOrganization() { + + return "apache"; + } + + @Override + protected String getGithubRepository() { + + return "maven-mvnd"; + } + + @Override + protected String getDownloadBaseUrl() { + + return "https://dlcdn.apache.org/maven/mvnd/"; + } + + @Override + protected boolean isVersionFiltered() { + // Don't filter pre-releases, we'll handle filtering explicitly in mapVersion() + return false; + } + + @Override + public String mapVersion(String version) { + // Accept pre-release versions (rc, beta, alpha, etc.) first + if (version.contains("-rc") || version.contains("-beta") || version.contains("-alpha")) { + return version; + } + + // For stable versions, require minimum version 1.0.2 + VersionIdentifier vid = VersionIdentifier.of(version); + if (vid.isValid()) { + VersionComparisonResult comparison = vid.compareVersion(MIN_MVND_VID); + if (comparison.isGreater() || comparison.isEqual()) { + return version; + } + } + return null; + } + + @Override + protected void addVersion(UrlVersion urlVersion) { + // Support both regular releases and pre-releases (e.g., 2.x versions) + // This allows users to test the latest versions before they become stable + String baseUrl = getDownloadBaseUrl() + "${version}/maven-mvnd-${version}-"; + + // Windows + doAddVersion(urlVersion, baseUrl + "windows-amd64.zip", WINDOWS, X64); + + // macOS + doAddVersion(urlVersion, baseUrl + "darwin-amd64.zip", MAC, X64); + doAddVersion(urlVersion, baseUrl + "darwin-aarch64.zip", MAC, ARM64); + + // Linux + doAddVersion(urlVersion, baseUrl + "linux-amd64.zip", LINUX, X64); + doAddVersion(urlVersion, baseUrl + "linux-aarch64.zip", LINUX, ARM64); + } +} diff --git a/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/AbstractUrlUpdater.java b/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/AbstractUrlUpdater.java index 9220373b61..605e43acc5 100644 --- a/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/AbstractUrlUpdater.java +++ b/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/AbstractUrlUpdater.java @@ -856,12 +856,14 @@ public String mapVersion(String version) { } String vLower = version.toLowerCase(Locale.ROOT); - if (vLower.contains("alpha") || vLower.contains("beta") || vLower.contains("dev") || vLower.contains("snapshot") || vLower.contains("preview") - || vLower.contains("test") || vLower.contains("tech-preview") // - || vLower.contains("-pre") || vLower.startsWith("ce-") || vLower.contains("-next") || vLower.contains("-rc") - // vscode nonsense - || vLower.startsWith("bad") || vLower.contains("vsda-") || vLower.contains("translation/") || vLower.contains("-insiders")) { - return null; + if (isVersionFiltered()) { + if (vLower.contains("alpha") || vLower.contains("beta") || vLower.contains("dev") || vLower.contains("snapshot") || vLower.contains("preview") + || vLower.contains("test") || vLower.contains("tech-preview") // + || vLower.contains("-pre") || vLower.startsWith("ce-") || vLower.contains("-next") || vLower.contains("-rc") + // vscode nonsense + || vLower.startsWith("bad") || vLower.contains("vsda-") || vLower.contains("translation/") || vLower.contains("-insiders")) { + return null; + } } String filter = getCustomVersionFilter(); @@ -889,6 +891,17 @@ protected String getCustomVersionFilter() { return null; } + /** + * Defines if we want to filter pre-releases and similar non-stable versions by default in {@link #filterVersion(String)}. Can be overridden to disable this + * default filtering. + * + * @return {@code true} if pre-releases and similar non-stable versions should be filtered by default, {@code false} otherwise. + */ + protected boolean isVersionFiltered() { + + return true; + } + /** * @param version the version to add (e.g. "1.0"). * @param versions the {@link Collection} with the versions to collect. diff --git a/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java b/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java index 067098b97f..836570bc29 100644 --- a/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java +++ b/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java @@ -39,6 +39,7 @@ import com.devonfw.tools.ide.url.tool.kotlinc.KotlincUrlUpdater; import com.devonfw.tools.ide.url.tool.lazydocker.LazyDockerUrlUpdater; import com.devonfw.tools.ide.url.tool.mvn.MvnUrlUpdater; +import com.devonfw.tools.ide.url.tool.mvnd.MvndUrlUpdater; import com.devonfw.tools.ide.url.tool.ng.NgUrlUpdater; import com.devonfw.tools.ide.url.tool.node.NodeUrlUpdater; import com.devonfw.tools.ide.url.tool.npm.NpmUrlUpdater; @@ -76,7 +77,7 @@ public class UpdateManager extends AbstractProcessorWithTimeout { new GcViewerUrlUpdater(), new GhUrlUpdater(), new GoUrlUpdater(), new GraalVmCommunityUpdater(), new GraalVmOracleUrlUpdater(), new GradleUrlUpdater(), new HelmUrlUpdater(), new IntellijUrlUpdater(), new JasyptUrlUpdater(), new JavaUrlUpdater(), new JavaAzulUrlUpdater(), new JenkinsUrlUpdater(), new JmcUrlUpdater(), new KotlincUrlUpdater(), - new KotlincNativeUrlUpdater(), new LazyDockerUrlUpdater(), new MvnUrlUpdater(), + new KotlincNativeUrlUpdater(), new LazyDockerUrlUpdater(), new MvnUrlUpdater(), new MvndUrlUpdater(), new NgUrlUpdater(), new NodeUrlUpdater(), new NpmUrlUpdater(), new OcUrlUpdater(), new PgAdminUrlUpdater(), new PipUrlUpdater(), new PycharmUrlUpdater(), new PythonUrlUpdater(), new QuarkusUrlUpdater(), new RustUrlUpdater(), new DockerRancherDesktopUrlUpdater(), new SonarUrlUpdater(), new SquirrelSqlUrlUpdater(), diff --git a/url-updater/src/test/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdaterMock.java b/url-updater/src/test/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdaterMock.java new file mode 100644 index 0000000000..87be1144cb --- /dev/null +++ b/url-updater/src/test/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdaterMock.java @@ -0,0 +1,29 @@ +package com.devonfw.tools.ide.url.tool.mvnd; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; + +/** + * Mock of {@link MvndUrlUpdater} to allow integration testing with wiremock. + */ +@SuppressWarnings("unused") +public class MvndUrlUpdaterMock extends MvndUrlUpdater { + + private final String baseUrl; + + MvndUrlUpdaterMock(WireMockRuntimeInfo wireMockRuntimeInfo) { + super(); + this.baseUrl = wireMockRuntimeInfo.getHttpBaseUrl(); + } + + @Override + protected String getVersionBaseUrl() { + return this.baseUrl + "/repos/"; + } + + @Override + protected String getDownloadBaseUrl() { + return this.baseUrl + "/maven/mvnd/"; + } +} + + diff --git a/url-updater/src/test/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdaterTest.java b/url-updater/src/test/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdaterTest.java new file mode 100644 index 0000000000..c236e0c7f1 --- /dev/null +++ b/url-updater/src/test/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdaterTest.java @@ -0,0 +1,112 @@ +package com.devonfw.tools.ide.url.tool.mvnd; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.devonfw.tools.ide.url.model.folder.UrlRepository; +import com.devonfw.tools.ide.url.updater.AbstractUrlUpdaterTest; +import com.devonfw.tools.ide.url.updater.JsonUrlUpdater; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; + +/** + * Test of {@link MvndUrlUpdater} + */ +@WireMockTest +@SuppressWarnings("unused") +class MvndUrlUpdaterTest extends AbstractUrlUpdaterTest { + + /** + * Test of {@link JsonUrlUpdater} for the creation of {@link MvndUrlUpdater} download URLs and checksums. + * + * @param tempDir Path to a temporary directory + * @param wmRuntimeInfo the {@link WireMockRuntimeInfo}. + * @throws IOException test fails + */ + @Test + void testMvndJsonUrlUpdaterCreatesDownloadUrlsAndChecksums(@TempDir Path tempDir, WireMockRuntimeInfo wmRuntimeInfo) throws IOException { + + // arrange + stubFor(get(urlMatching("/repos/apache/maven-mvnd/releases")).willReturn( + aResponse().withStatus(200).withBody(getJsonBody(wmRuntimeInfo)))); + + stubFor(any(urlMatching("/maven/mvnd/.*\\.zip")).willReturn(aResponse().withStatus(200).withBody(DOWNLOAD_CONTENT))); + + UrlRepository urlRepository = UrlRepository.load(tempDir); + MvndUrlUpdaterMock updater = new MvndUrlUpdaterMock(wmRuntimeInfo); + + // act + updater.update(urlRepository); + + // assert + assertUrlVersionOsX64MacArm(tempDir.resolve("mvnd").resolve("mvnd").resolve("1.0.5")); + } + + /** + * Test if the {@link JsonUrlUpdater} for {@link MvndUrlUpdater} can handle filtering of old versions. + * + * @param tempDir Path to a temporary directory + * @param wmRuntimeInfo the {@link WireMockRuntimeInfo}. + * @throws IOException test fails + */ + @Test + void testMvndJsonUrlUpdaterFilterOldVersions(@TempDir Path tempDir, WireMockRuntimeInfo wmRuntimeInfo) throws IOException { + + // arrange + stubFor(get(urlMatching("/repos/apache/maven-mvnd/releases")).willReturn( + aResponse().withStatus(200).withBody(getJsonBody(wmRuntimeInfo)))); + + stubFor(any(urlMatching("/maven/mvnd/.*\\.zip")).willReturn(aResponse().withStatus(200).withBody(DOWNLOAD_CONTENT))); + + UrlRepository urlRepository = UrlRepository.load(tempDir); + MvndUrlUpdaterMock updater = new MvndUrlUpdaterMock(wmRuntimeInfo); + + // act + updater.update(urlRepository); + + // assert + Path mvndOldVersionPath = tempDir.resolve("mvnd").resolve("mvnd").resolve("1.0.0"); + assertThat(mvndOldVersionPath).doesNotExist(); + } + + /** + * Test if the {@link JsonUrlUpdater} for {@link MvndUrlUpdater} accepts pre-release versions (rc). + * + * @param tempDir Path to a temporary directory + * @param wmRuntimeInfo the {@link WireMockRuntimeInfo}. + * @throws IOException test fails + */ + @Test + void testMvndJsonUrlUpdaterAcceptsReleaseCandidate(@TempDir Path tempDir, WireMockRuntimeInfo wmRuntimeInfo) throws IOException { + + // arrange + stubFor(get(urlMatching("/repos/apache/maven-mvnd/releases")).willReturn( + aResponse().withStatus(200).withBody(getJsonBody(wmRuntimeInfo)))); + + stubFor(any(urlMatching("/maven/mvnd/.*\\.zip")).willReturn(aResponse().withStatus(200).withBody(DOWNLOAD_CONTENT))); + + UrlRepository urlRepository = UrlRepository.load(tempDir); + MvndUrlUpdaterMock updater = new MvndUrlUpdaterMock(wmRuntimeInfo); + + // act + updater.update(urlRepository); + + // assert + Path mvndRcVersionPath = tempDir.resolve("mvnd").resolve("mvnd").resolve("2.0.0-rc-3"); + assertUrlVersionOsX64MacArm(mvndRcVersionPath); + } + + private static String getJsonBody(WireMockRuntimeInfo wmRuntimeInfo) throws IOException { + return readAndResolve(PATH_INTEGRATION_TEST.resolve("MvndUrlUpdater").resolve("mvnd-releases.json"), wmRuntimeInfo); + } +} diff --git a/url-updater/src/test/resources/integrationtest/MvndUrlUpdater/mvnd-releases.json b/url-updater/src/test/resources/integrationtest/MvndUrlUpdater/mvnd-releases.json new file mode 100644 index 0000000000..30955f4ce2 --- /dev/null +++ b/url-updater/src/test/resources/integrationtest/MvndUrlUpdater/mvnd-releases.json @@ -0,0 +1,72 @@ +[ + { + "url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123456", + "assets_url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123456/assets", + "upload_url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123456/assets{?name,label}", + "html_url": "${testbaseurl}/apache/maven-mvnd/releases/tag/1.0.5", + "id": 123456, + "tag_name": "1.0.5", + "target_commitish": "main", + "name": "1.0.5", + "draft": false, + "prerelease": false, + "created_at": "2026-03-17T12:00:00Z", + "published_at": "2026-03-17T12:09:00Z", + "assets": [], + "body": "Maven Daemon 1.0.5" + }, + { + "url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123457", + "assets_url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123457/assets", + "upload_url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123457/assets{?name,label}", + "html_url": "${testbaseurl}/apache/maven-mvnd/releases/tag/1.0.4", + "id": 123457, + "tag_name": "1.0.4", + "target_commitish": "main", + "name": "1.0.4", + "draft": false, + "prerelease": false, + "created_at": "2026-03-10T12:00:00Z", + "published_at": "2026-03-10T12:09:00Z", + "assets": [], + "body": "Maven Daemon 1.0.4" + }, + { + "url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123458", + "assets_url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123458/assets", + "upload_url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123458/assets{?name,label}", + "html_url": "${testbaseurl}/apache/maven-mvnd/releases/tag/2.0.0-rc-3", + "id": 123458, + "tag_name": "2.0.0-rc-3", + "target_commitish": "main", + "name": "2.0.0-rc-3", + "draft": false, + "prerelease": true, + "created_at": "2026-03-05T12:00:00Z", + "published_at": "2026-03-05T12:09:00Z", + "assets": [], + "body": "Maven Daemon 2.0.0-rc-3" + }, + { + "url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123459", + "assets_url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123459/assets", + "upload_url": "${testbaseurl}/repos/apache/maven-mvnd/releases/123459/assets{?name,label}", + "html_url": "${testbaseurl}/apache/maven-mvnd/releases/tag/1.0.0", + "id": 123459, + "tag_name": "1.0.0", + "target_commitish": "main", + "name": "1.0.0", + "draft": false, + "prerelease": false, + "created_at": "2026-01-01T12:00:00Z", + "published_at": "2026-01-01T12:09:00Z", + "assets": [], + "body": "Maven Daemon 1.0.0" + } +] + + + + + +