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..c1e066c94e --- /dev/null +++ b/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/mvnd/MvndUrlUpdater.java @@ -0,0 +1,77 @@ +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 isAcceptPreVersion() { + + return true; + } + + @Override + public String mapVersion(String version) { + + String mappedVersion = super.mapVersion(version); + if (mappedVersion != null) { + // Require minimum version (1.0.2) + VersionIdentifier vid = VersionIdentifier.of(mappedVersion); + if (vid.isGreaterOrEqual(MIN_MVND_VID)) { + return mappedVersion; + } + } + 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..754b0da930 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,11 +856,7 @@ 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")) { + if (!isAcceptVersion(vLower)) { return null; } @@ -871,6 +867,32 @@ public String mapVersion(String version) { return version; } + /** + * @param version the version to check in lower-case (e.g. "1.0" or "1.0-alpha1"). + * @return {@code true} if version should be accepted, {@code false} otherwise (to filter and ignore this version). + */ + protected boolean isAcceptVersion(String version) { + + if (version.contains("alpha") || version.contains("beta") || version.contains("dev") || version.contains("snapshot") || version.contains("preview") + || version.contains("test") || version.contains("tech-preview") // + || version.startsWith("ce-") || version.contains("-next") + // vscode nonsense + || version.startsWith("bad") || version.contains("vsda-") || version.contains("translation/") || version.contains("-insiders")) { + return false; + } + if (version.contains("-rc") || version.contains("-pre")) { + return isAcceptPreVersion(); + } + return true; + } + + /** + * @return {@code true} to accept release-candidate ("rc") or "pre" versions, {@code false} otherwise (default). + */ + protected boolean isAcceptPreVersion() { + + return false; + } /** * @return the optional version prefix that has to be removed (e.g. "v"). @@ -880,7 +902,6 @@ protected String getVersionPrefixToRemove() { return null; } - /** * @return the generic filters applied in {@link #filterVersion(String)}. Example: a tool might want to exclude versions containing "rc" or "beta". */ 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 1f7e2407ae..5f51a4e10d 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 @@ -41,6 +41,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,9 +77,9 @@ public class UpdateManager extends AbstractProcessorWithTimeout { new DotNetUrlUpdater(), new EclipseCppUrlUpdater(), new EclipseJeeUrlUpdater(), new EclipseJavaUrlUpdater(), new GCloudUrlUpdater(), new GcViewerUrlUpdater(), new GhUrlUpdater(), new GoUrlUpdater(), new GraalVmCommunityUpdater(), new GraalVmOracleUrlUpdater(), - new GradleUrlUpdater(), new HelmUrlUpdater(),new InsoUrlUpdater(), new IntellijUrlUpdater(), new JasyptUrlUpdater(),new JavaAzulUrlUpdater(), + new GradleUrlUpdater(), new HelmUrlUpdater(), new InsoUrlUpdater(), new IntellijUrlUpdater(), new JasyptUrlUpdater(), new JavaAzulUrlUpdater(), new JavaUrlUpdater(), 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" + } +] + + + + + +