diff --git a/cli/pom.xml b/cli/pom.xml
index f7b8107eb..039ab74ec 100644
--- a/cli/pom.xml
+++ b/cli/pom.xml
@@ -6,7 +6,6 @@
com.devonfw.tools.IDEasy.dev
ide
dev-SNAPSHOT
- ../pom.xml
com.devonfw.tools.IDEasy
ide-cli
@@ -246,8 +245,9 @@
${imageName}
--enable-url-protocols=http,https
- --initialize-at-build-time=org.apache.commons
-march=compatibility
+ --initialize-at-build-time=org.apache.commons
+ -H:IncludeResourceBundles=com.sun.org.apache.xml.internal.res.XMLErrorResources,com.sun.org.apache.xerces.internal.impl.msg.XMLMessages
diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java
index 8103b3ae3..9a7d44b6a 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java
@@ -93,6 +93,7 @@ public CommandletManagerImpl(IdeContext context) {
add(new BuildCommandlet(context));
add(new InstallPluginCommandlet(context));
add(new UninstallPluginCommandlet(context));
+ add(new UpgradeCommandlet(context));
add(new Gh(context));
add(new Helm(context));
add(new Java(context));
diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpgradeCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpgradeCommandlet.java
new file mode 100644
index 000000000..63ebc57ae
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpgradeCommandlet.java
@@ -0,0 +1,157 @@
+package com.devonfw.tools.ide.commandlet;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.os.WindowsPathSyntax;
+import com.devonfw.tools.ide.process.ProcessContext;
+import com.devonfw.tools.ide.process.ProcessMode;
+import com.devonfw.tools.ide.repo.MavenRepository;
+import com.devonfw.tools.ide.version.IdeVersion;
+import com.devonfw.tools.ide.version.VersionIdentifier;
+
+/**
+ * {@link Commandlet} to upgrade the version of IDEasy
+ */
+public class UpgradeCommandlet extends Commandlet {
+
+ private static final VersionIdentifier LATEST_SNAPSHOT = VersionIdentifier.of("*-SNAPSHOT");
+ public static final String IDEASY = "ideasy";
+
+ /**
+ * The constructor.
+ *
+ * @param context the {@link IdeContext}.
+ */
+ public UpgradeCommandlet(IdeContext context) {
+
+ super(context);
+ addKeyword(getName());
+ }
+
+ @Override
+ public String getName() {
+
+ return "upgrade";
+ }
+
+ /**
+ * Compares two snapshot versions to determine if the latest is newer. Handles versions in the following formats: - Current version format:
+ * "2024.12.002-beta-12_18_02-SNAPSHOT" - Latest version format: "2025.01.001-beta-20250118.022832-8"
+ *
+ * First compares base versions (e.g. 2024.12.002 with 2025.01.001), then timestamps if base versions are equal. Returns false if version formats are
+ * unexpected to avoid unintended upgrades.
+ *
+ * @param currentVersion The current snapshot version
+ * @param latestVersion The latest snapshot version to compare against
+ * @return true if latestVersion is newer than currentVersion, false otherwise or if formats are invalid
+ */
+ protected boolean isSnapshotNewer(String currentVersion, String latestVersion) {
+
+ try {
+ // Validate input formats
+ if (currentVersion == null || latestVersion == null || !currentVersion.contains("-") || !latestVersion.contains(
+ "-")) {
+ return false;
+ }
+
+ // First compare base versions (2024.12.002 with 2025.01.001)
+ String currentBase = currentVersion.substring(0, currentVersion.indexOf('-'));
+ String latestBase = latestVersion.substring(0, latestVersion.indexOf('-'));
+
+ VersionIdentifier currentBaseVersion = VersionIdentifier.of(currentBase);
+ VersionIdentifier latestBaseVersion = VersionIdentifier.of(latestBase);
+
+ // If base versions are different, use regular version comparison
+ if (!currentBaseVersion.compareVersion(latestBaseVersion).isEqual()) {
+ return currentBaseVersion.compareVersion(latestBaseVersion).isLess();
+ }
+
+ // Validate timestamp formats
+ String[] currentParts = currentVersion.split("-");
+ String[] latestParts = latestVersion.split("-");
+ if (currentParts.length < 3 || latestParts.length < 3) {
+ return false;
+ }
+
+ // Extract timestamps
+ String currentTimestamp = currentParts[2].split("-")[0].replace("_", ""); // "010102"
+ String[] latestTimestampParts = latestParts[2].split("\\.");
+ if (latestTimestampParts.length < 1) {
+ return false;
+ }
+
+ // Get year from base version (2024.12.002 -> 2024)
+ String year = currentBase.substring(0, 4);
+
+ // Parse current date/time using extracted year (currentTimestamp format: MMDDXX)
+ LocalDateTime currentTime = LocalDateTime.parse(year + currentTimestamp + "00", // YYYYMMDDHHmm
+ DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
+
+ // Parse latest date/time (format: YYYYMMDD.HHMMSS)
+ LocalDateTime latestTime = LocalDateTime.parse(latestTimestampParts[0] + "000000",
+ DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+
+ return latestTime.isAfter(currentTime);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to compare current and latest Snapshot.", e);
+ }
+ }
+
+ @Override
+ public void run() {
+
+ String version = IdeVersion.get();
+ if (IdeVersion.VERSION_UNDEFINED.equals(version)) {
+ this.context.warning("You are using IDEasy version {} what indicates local development - skipping upgrade.", version);
+ return;
+ }
+ VersionIdentifier currentVersion = VersionIdentifier.of(version);
+ MavenRepository mavenRepo = this.context.getMavenSoftwareRepository();
+ VersionIdentifier configuredVersion;
+ if (version.contains("SNAPSHOT")) {
+ configuredVersion = LATEST_SNAPSHOT;
+ } else if (currentVersion.getDevelopmentPhase().isStable()) {
+ configuredVersion = VersionIdentifier.LATEST;
+ } else {
+ configuredVersion = VersionIdentifier.LATEST_UNSTABLE;
+ }
+ this.context.debug("Trying to determine the latest version of IDEasy ({})", configuredVersion);
+ VersionIdentifier resolvedVersion = mavenRepo.resolveVersion(IDEASY, IDEASY, configuredVersion);
+
+ boolean upgradeAvailable = resolvedVersion.isGreater(currentVersion);
+ if (upgradeAvailable) {
+ this.context.info("Upgrading IDEasy from version {} to {}", version, resolvedVersion);
+ try {
+ this.context.info("Downloading new version...");
+ Path downloadTarget = mavenRepo.download(IDEASY, IDEASY, resolvedVersion);
+ Path extractionTarget = this.context.getIdeRoot().resolve(IdeContext.FOLDER_IDE);
+ if (this.context.getSystemInfo().isWindows()) {
+ handleUpgradeOnWindows(downloadTarget, extractionTarget);
+ } else {
+ this.context.info("Extracting files...");
+ this.context.getFileAccess().extract(downloadTarget, extractionTarget);
+ this.context.success("Successfully upgraded to version {}", resolvedVersion);
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to upgrade version.", e);
+ }
+ } else {
+ this.context.info("Your have IDEasy {} installed what is already the latest version.", version);
+ }
+ }
+
+ private void handleUpgradeOnWindows(Path downloadTarget, Path extractionTarget) throws IOException {
+
+ ProcessContext pc = this.context.newProcess().executable("bash")
+ .addArgs("-c",
+ "'sleep 10;tar xvfz \"" + WindowsPathSyntax.MSYS.format(downloadTarget) + "\" -C \"" + WindowsPathSyntax.MSYS.format(extractionTarget) + "\"'");
+ pc.run(ProcessMode.BACKGROUND_SILENT);
+ this.context.interaction("To prevent windows file locking errors, "
+ + "we perform an asynchronous upgrade in background now.\n"
+ + "Please wait a minute for the upgrade to complete before running IDEasy commands.");
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
index 52143744b..12cc87f99 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
@@ -53,6 +53,7 @@
import com.devonfw.tools.ide.repo.CustomToolRepository;
import com.devonfw.tools.ide.repo.CustomToolRepositoryImpl;
import com.devonfw.tools.ide.repo.DefaultToolRepository;
+import com.devonfw.tools.ide.repo.MavenRepository;
import com.devonfw.tools.ide.repo.ToolRepository;
import com.devonfw.tools.ide.step.Step;
import com.devonfw.tools.ide.step.StepImpl;
@@ -128,6 +129,8 @@ public abstract class AbstractIdeContext implements IdeContext {
private CustomToolRepository customToolRepository;
+ private MavenRepository mavenRepository;
+
private DirectoryMerger workspaceMerger;
protected UrlMetadata urlMetadata;
@@ -207,6 +210,7 @@ public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirector
}
this.defaultToolRepository = new DefaultToolRepository(this);
+ this.mavenRepository = new MavenRepository(this);
}
private Path findIdeRoot(Path ideHomePath) {
@@ -354,6 +358,12 @@ public ToolRepository getDefaultToolRepository() {
return this.defaultToolRepository;
}
+ @Override
+ public MavenRepository getMavenSoftwareRepository() {
+
+ return this.mavenRepository;
+ }
+
@Override
public CustomToolRepository getCustomToolRepository() {
@@ -439,6 +449,7 @@ public Path getSettingsGitRepository() {
return settingsPath;
}
+ @Override
public boolean isSettingsRepositorySymlinkOrJunction() {
Path settingsPath = getSettingsPath();
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java
index bc2e00a4b..3435021e3 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java
@@ -19,6 +19,7 @@
import com.devonfw.tools.ide.os.WindowsPathSyntax;
import com.devonfw.tools.ide.process.ProcessContext;
import com.devonfw.tools.ide.repo.CustomToolRepository;
+import com.devonfw.tools.ide.repo.MavenRepository;
import com.devonfw.tools.ide.repo.ToolRepository;
import com.devonfw.tools.ide.step.Step;
import com.devonfw.tools.ide.tool.mvn.Mvn;
@@ -273,6 +274,11 @@ default void requireOnline(String purpose) {
*/
CustomToolRepository getCustomToolRepository();
+ /**
+ * @return the {@link MavenRepository}.
+ */
+ MavenRepository getMavenSoftwareRepository();
+
/**
* @return the {@link Path} to the IDE instance directory. You can have as many IDE instances on the same computer as independent tenants for different
* isolated projects.
diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/AbstractToolRepository.java b/cli/src/main/java/com/devonfw/tools/ide/repo/AbstractToolRepository.java
index 41c5d4976..ddab7b11b 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/repo/AbstractToolRepository.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/repo/AbstractToolRepository.java
@@ -44,6 +44,7 @@ public AbstractToolRepository(IdeContext context) {
*/
protected abstract UrlDownloadFileMetadata getMetadata(String tool, String edition, VersionIdentifier version);
+
@Override
public Path download(String tool, String edition, VersionIdentifier version) {
diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/MavenArtifactMetadata.java b/cli/src/main/java/com/devonfw/tools/ide/repo/MavenArtifactMetadata.java
new file mode 100644
index 000000000..c2d89a964
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/repo/MavenArtifactMetadata.java
@@ -0,0 +1,87 @@
+package com.devonfw.tools.ide.repo;
+
+import java.util.Collections;
+import java.util.Set;
+
+import com.devonfw.tools.ide.os.OperatingSystem;
+import com.devonfw.tools.ide.os.SystemArchitecture;
+import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
+import com.devonfw.tools.ide.url.model.file.UrlDownloadFileMetadata;
+import com.devonfw.tools.ide.version.VersionIdentifier;
+
+/**
+ * {@link UrlDownloadFileMetadata} representing Metadata of a maven artifact.
+ */
+public class MavenArtifactMetadata implements UrlDownloadFileMetadata {
+
+ private final MvnArtifact mvnArtifact;
+
+ private final VersionIdentifier version;
+
+ private final OperatingSystem os;
+
+ private final SystemArchitecture arch;
+
+ MavenArtifactMetadata(MvnArtifact mvnArtifact) {
+
+ this(mvnArtifact, null, null);
+ }
+
+ MavenArtifactMetadata(MvnArtifact mvnArtifact, OperatingSystem os, SystemArchitecture arch) {
+
+ this.mvnArtifact = mvnArtifact;
+ this.version = VersionIdentifier.of(mvnArtifact.getVersion());
+ this.os = os;
+ this.arch = arch;
+ }
+
+ /**
+ * @return the {@link MvnArtifact}.
+ */
+ public MvnArtifact getMvnArtifact() {
+
+ return this.mvnArtifact;
+ }
+
+ @Override
+ public String getTool() {
+
+ return this.mvnArtifact.getGroupId();
+ }
+
+ @Override
+ public String getEdition() {
+
+ return this.mvnArtifact.getArtifactId();
+ }
+
+ @Override
+ public VersionIdentifier getVersion() {
+
+ return this.version;
+ }
+
+ @Override
+ public Set getUrls() {
+
+ return Collections.singleton(this.mvnArtifact.getDownloadUrl());
+ }
+
+ @Override
+ public OperatingSystem getOs() {
+
+ return this.os;
+ }
+
+ @Override
+ public SystemArchitecture getArch() {
+
+ return this.arch;
+ }
+
+ @Override
+ public String getChecksum() {
+
+ return null;
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/MavenRepository.java b/cli/src/main/java/com/devonfw/tools/ide/repo/MavenRepository.java
new file mode 100644
index 000000000..76716be64
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/repo/MavenRepository.java
@@ -0,0 +1,195 @@
+package com.devonfw.tools.ide.repo;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.os.OperatingSystem;
+import com.devonfw.tools.ide.os.SystemArchitecture;
+import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
+import com.devonfw.tools.ide.url.model.file.UrlDownloadFileMetadata;
+import com.devonfw.tools.ide.url.model.file.json.ToolDependency;
+import com.devonfw.tools.ide.version.GenericVersionRange;
+import com.devonfw.tools.ide.version.VersionIdentifier;
+
+/**
+ * Implementation of {@link AbstractToolRepository} for maven-based artifacts.
+ */
+public class MavenRepository extends AbstractToolRepository {
+
+ /** Base URL for Maven Central repository */
+ public static final String MAVEN_CENTRAL = "https://repo1.maven.org/maven2";
+
+ /** Base URL for Maven Snapshots repository */
+ public static final String MAVEN_SNAPSHOTS = "https://s01.oss.sonatype.org/content/repositories/snapshots";
+
+ private final DocumentBuilder documentBuilder;
+
+ private static final Map TOOL_MAP = Map.of(
+ "ideasy", MvnArtifact.ofIdeasyCli("*", "tar.gz", "${os}-${arch}"),
+ "gcviewer", new MvnArtifact("com.github.chewiebug", "gcviewer", "*")
+ );
+
+ private MavenArtifactMetadata resolveArtifact(String tool, String edition, VersionIdentifier version) {
+
+ String key = tool;
+ if (!tool.equals(edition)) {
+ key = tool + ":" + edition;
+ }
+ MvnArtifact artifact = TOOL_MAP.get(key);
+ if (artifact == null) {
+ throw new UnsupportedOperationException("Tool '" + key + "' is not supported by Maven repository.");
+ }
+ OperatingSystem os = null;
+ SystemArchitecture arch = null;
+ String classifier = artifact.getClassifier();
+ if (!classifier.isEmpty()) {
+ String resolvedClassifier;
+ os = this.context.getSystemInfo().getOs();
+ resolvedClassifier = classifier.replace("${os}", os.toString());
+ if (resolvedClassifier.equals(classifier)) {
+ os = null;
+ } else {
+ classifier = resolvedClassifier;
+ }
+ arch = this.context.getSystemInfo().getArchitecture();
+ resolvedClassifier = classifier.replace("${arch}", arch.toString());
+ if (resolvedClassifier.equals(classifier)) {
+ arch = null;
+ }
+ artifact = artifact.withClassifier(resolvedClassifier);
+ }
+ if (version != null) {
+ artifact = artifact.withVersion(version.toString());
+ }
+ return new MavenArtifactMetadata(artifact, os, arch);
+ }
+
+ /**
+ * The constructor.
+ *
+ * @param context the owning {@link IdeContext}.
+ */
+ public MavenRepository(IdeContext context) {
+
+ super(context);
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ this.documentBuilder = factory.newDocumentBuilder();
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to create XML document builder", e);
+ }
+ }
+
+ @Override
+ protected UrlDownloadFileMetadata getMetadata(String tool, String edition, VersionIdentifier version) {
+
+ return resolveArtifact(tool, edition, version);
+ }
+
+
+ @Override
+ public VersionIdentifier resolveVersion(String tool, String edition, GenericVersionRange version) {
+
+ MavenArtifactMetadata artifactMetadata = resolveArtifact(tool, edition, null);
+ MvnArtifact artifact = artifactMetadata.getMvnArtifact();
+ return resolveVersion(artifact, version);
+ }
+
+ /**
+ * @param artifact the {@link MvnArtifact} to resolve. Should not have a version set.
+ * @param version the {@link GenericVersionRange} to resolve.
+ * @return the resolved {@link VersionIdentifier}.
+ */
+ public VersionIdentifier resolveVersion(MvnArtifact artifact, GenericVersionRange version) {
+
+ artifact = artifact.withMavenMetadata();
+ String versionString = version.toString();
+ if (versionString.startsWith("*")) {
+ artifact = artifact.withVersion(versionString);
+ }
+ List versions = fetchVersions(artifact.getDownloadUrl());
+ VersionIdentifier resolvedVersion = this.context.getUrls().resolveVersionPattern(version, versions);
+ versionString = resolvedVersion.toString();
+ if (versionString.endsWith("-SNAPSHOT")) {
+ artifact = artifact.withVersion(versionString);
+ return resolveSnapshotVersion(artifact.getDownloadUrl(), versionString);
+ }
+ return resolvedVersion;
+ }
+
+ private List fetchVersions(String metadataUrl) {
+ try {
+ Document doc = fetchXmlMetadata(metadataUrl);
+ XPath xpath = XPathFactory.newInstance().newXPath();
+ NodeList versions = (NodeList) xpath.evaluate("//versions/version", doc, XPathConstants.NODESET);
+
+ List versionList = new ArrayList<>();
+ for (int i = 0; i < versions.getLength(); i++) {
+ versionList.add(VersionIdentifier.of(versions.item(i).getTextContent()));
+ }
+ versionList.sort(Comparator.reverseOrder());
+ return versionList;
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to fetch versions from " + metadataUrl, e);
+ }
+ }
+
+ private VersionIdentifier resolveSnapshotVersion(String metadataUrl, String baseVersion) {
+ try {
+ Document doc = fetchXmlMetadata(metadataUrl);
+ XPath xpath = XPathFactory.newInstance().newXPath();
+ String timestamp = (String) xpath.evaluate("//timestamp", doc, XPathConstants.STRING);
+ String buildNumber = (String) xpath.evaluate("//buildNumber", doc, XPathConstants.STRING);
+
+ if (timestamp.isEmpty() || buildNumber.isEmpty()) {
+ throw new IllegalStateException("Missing timestamp or buildNumber in snapshot metadata");
+ }
+
+ String version = baseVersion.replace("-SNAPSHOT", "-" + timestamp + "-" + buildNumber);
+ return VersionIdentifier.of(version);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to resolve snapshot version for " + baseVersion, e);
+ }
+ }
+
+ private Document fetchXmlMetadata(String url) {
+
+ try {
+ URL xmlUrl = new URL(url);
+ try (InputStream is = xmlUrl.openStream()) {
+ return documentBuilder.parse(is);
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to fetch XML metadata from " + url, e);
+ }
+ }
+
+ @Override
+ public String getId() {
+
+ return "maven";
+ }
+
+ @Override
+ public Collection findDependencies(String groupId, String artifactId, VersionIdentifier version) {
+
+ // We could read POM here and find dependencies but we do not want to reimplement maven here.
+ // For our use-case we only download bundled packages from maven central so we do KISS for now.
+ return Collections.emptyList();
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/MvnArtifact.java b/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/MvnArtifact.java
new file mode 100644
index 000000000..a8c6c434d
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/MvnArtifact.java
@@ -0,0 +1,340 @@
+package com.devonfw.tools.ide.tool.mvn;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.devonfw.tools.ide.repo.MavenRepository;
+
+/**
+ * Simple type representing a maven artifact.
+ */
+public final class MvnArtifact {
+
+ /** {@link #getGroupId() Group ID} of IDEasy. */
+ public static final String GROUP_ID_IDEASY = "com.devonfw.tools.IDEasy";
+
+ /** {@link #getArtifactId() Artifact ID} of IDEasy command line interface. */
+ public static final String ARTIFACT_ID_IDEASY_CLI = "ide-cli";
+
+ /** {@link #getClassifier() Classifier} of source code. */
+ public static final String CLASSIFER_SOURCES = "sources";
+
+ /** {@link #getType() Type} of JAR file. */
+ public static final String TYPE_JAR = "jar";
+
+ /** {@link #getType() Type} of POM XML file. */
+ public static final String TYPE_POM = "pom";
+
+ /** {@link #getFilename() Filename} for artifact metadata with version information. */
+ public static final String MAVEN_METADATA_XML = "maven-metadata.xml";
+
+ private static final Pattern SNAPSHOT_VERSION_PATTERN = Pattern.compile("-\\d{8}\\.\\d{6}-\\d+");
+
+ private final String groupId;
+
+ private final String artifactId;
+
+ private final String version;
+
+ private final String classifier;
+
+ private final String type;
+
+ private final String filename;
+
+ private String path;
+
+ private String key;
+
+ private String downloadUrl;
+
+ /**
+ * The constructor.
+ *
+ * @param groupId the {@link #getGroupId() group ID}.
+ * @param artifactId the {@link #getArtifactId() artifact ID}.
+ * @param version the {@link #getVersion() version}.
+ */
+ public MvnArtifact(String groupId, String artifactId, String version) {
+ this(groupId, artifactId, version, TYPE_JAR);
+ }
+
+ /**
+ * The constructor.
+ *
+ * @param groupId the {@link #getGroupId() group ID}.
+ * @param artifactId the {@link #getArtifactId() artifact ID}.
+ * @param version the {@link #getVersion() version}.
+ * @param type the {@link #getType() type}.
+ */
+ public MvnArtifact(String groupId, String artifactId, String version, String type) {
+ this(groupId, artifactId, version, type, "");
+ }
+
+ /**
+ * The constructor.
+ *
+ * @param groupId the {@link #getGroupId() group ID}.
+ * @param artifactId the {@link #getArtifactId() artifact ID}.
+ * @param version the {@link #getVersion() version}.
+ * @param type the {@link #getType() type}.
+ * @param classifier the {@link #getClassifier() classifier}.
+ */
+ public MvnArtifact(String groupId, String artifactId, String version, String type, String classifier) {
+ this(groupId, artifactId, version, type, classifier, null);
+ }
+
+ MvnArtifact(String groupId, String artifactId, String version, String type, String classifier, String filename) {
+ super();
+ this.groupId = requireNotEmpty(groupId, "groupId");
+ this.artifactId = requireNotEmpty(artifactId, "artifactId");
+ this.version = requireNotEmpty(version, "version");
+ this.classifier = notNull(classifier);
+ this.type = requireNotEmpty(type, "type");
+ this.filename = filename;
+ }
+
+ /**
+ * @return the group ID (e.g. {@link #GROUP_ID_IDEASY}).
+ */
+ public String getGroupId() {
+ return this.groupId;
+ }
+
+ /**
+ * @return the artifact ID (e.g. {@link #ARTIFACT_ID_IDEASY_CLI}).
+ */
+ public String getArtifactId() {
+ return this.artifactId;
+ }
+
+ /**
+ * @return the version.
+ * @see com.devonfw.tools.ide.version.VersionIdentifier
+ */
+ public String getVersion() {
+ return this.version;
+ }
+
+ /**
+ * @param newVersion the new value of {@link #getVersion()}.
+ * @return a new {@link MvnArtifact} with the given version.
+ */
+ public MvnArtifact withVersion(String newVersion) {
+
+ if (this.version.equals(newVersion)) {
+ return this;
+ }
+ return new MvnArtifact(this.groupId, this.artifactId, newVersion, this.type, this.classifier, this.filename);
+ }
+
+ /**
+ * @return the classifier. Will be the empty {@link String} for no classifier.
+ */
+ public String getClassifier() {
+ return this.classifier;
+ }
+
+ /**
+ * @param newClassifier the new value of {@link #getClassifier()}.
+ * @return a new {@link MvnArtifact} with the given classifier.
+ */
+ public MvnArtifact withClassifier(String newClassifier) {
+
+ if (this.classifier.equals(newClassifier)) {
+ return this;
+ }
+ return new MvnArtifact(this.groupId, this.artifactId, this.version, this.type, newClassifier, this.filename);
+ }
+
+ /**
+ * @return the type (e.g. #TYPE_JAR}
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * @param newType the new value of {@link #getType()}.
+ * @return a new {@link MvnArtifact} with the given type.
+ */
+ public MvnArtifact withType(String newType) {
+
+ if (this.type.equals(newType)) {
+ return this;
+ }
+ return new MvnArtifact(this.groupId, this.artifactId, this.version, newType, this.classifier, this.filename);
+ }
+
+ /**
+ * @return the filename of the artifact.
+ */
+ public String getFilename() {
+
+ if (this.filename == null) {
+ String infix = "";
+ if (!this.classifier.isEmpty()) {
+ infix = "-" + this.classifier;
+ }
+ return this.artifactId + "-" + this.version + infix + "." + this.type;
+ }
+ return this.filename;
+ }
+
+ /**
+ * @param newFilename the new value of {@link #getFilename()}.
+ * @return a new {@link MvnArtifact} with the given filename.
+ */
+ public MvnArtifact withFilename(String newFilename) {
+
+ if (Objects.equals(this.filename, newFilename)) {
+ return this;
+ }
+ return new MvnArtifact(this.groupId, this.artifactId, this.version, this.type, this.classifier, newFilename);
+ }
+
+ /**
+ * @return a new {@link MvnArtifact} for {@link #MAVEN_METADATA_XML}.
+ */
+ public MvnArtifact withMavenMetadata() {
+
+ return withType("xml").withFilename(MAVEN_METADATA_XML);
+ }
+
+ /**
+ * @return the {@link String} with the path to the specified artifact relative to the maven repository base path or URL. For snapshots, includes the
+ * timestamped version in the artifact filename.
+ */
+ public String getPath() {
+ if (this.path == null) {
+ StringBuilder sb = new StringBuilder();
+ // Common path start: groupId/artifactId/version
+ sb.append(this.groupId.replace('.', '/')).append('/')
+ .append(this.artifactId).append('/');
+
+ if (!this.version.startsWith("*")) {
+ sb.append(getBaseVersion()).append('/');
+ }
+ sb.append(getFilename());
+ this.path = sb.toString();
+ }
+ return this.path;
+ }
+
+ /**
+ * @return the artifact key as unique identifier.
+ */
+ String getKey() {
+ if (this.key == null) {
+ int capacity = this.groupId.length() + this.artifactId.length() + this.version.length() + type.length() + classifier.length() + 4;
+ StringBuilder sb = new StringBuilder(capacity);
+ sb.append(this.groupId).append(':').append(this.artifactId).append(':').append(this.version).append(':').append(this.type);
+ if (!this.classifier.isEmpty()) {
+ sb.append(':').append(this.classifier);
+ }
+ this.key = sb.toString();
+ assert (this.key.length() <= capacity);
+ }
+ return this.key;
+ }
+
+ /**
+ * Checks if the current artifact version is a snapshot version.
+ *
+ * @return true if this is a snapshot version, false otherwise
+ */
+ public boolean isSnapshot() {
+ return this.version.endsWith("-SNAPSHOT") || SNAPSHOT_VERSION_PATTERN.matcher(this.version).find();
+ }
+
+ /**
+ * Gets the base version without snapshot timestamp. For snapshot versions like "2024.04.001-beta-20240419.123456-1", returns "2024.04.001-beta-SNAPSHOT". For
+ * release versions, returns the version as is.
+ *
+ * @return the base version
+ */
+ public String getBaseVersion() {
+ Matcher matcher = SNAPSHOT_VERSION_PATTERN.matcher(this.version);
+ if (matcher.find()) {
+ return matcher.replaceAll("-SNAPSHOT");
+ }
+ return this.version;
+ }
+
+ /**
+ * @return the download URL to download the artifact from the maven repository.
+ */
+ public String getDownloadUrl() {
+ if (this.downloadUrl == null) {
+ String baseUrl = isSnapshot() ? MavenRepository.MAVEN_SNAPSHOTS : MavenRepository.MAVEN_CENTRAL;
+ this.downloadUrl = baseUrl + "/" + getPath();
+ }
+ return this.downloadUrl;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.groupId, this.artifactId, this.version);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ } else if (obj instanceof MvnArtifact other) {
+ return this.groupId.equals(other.groupId) && this.artifactId.equals(other.artifactId) && this.version.equals(other.version)
+ && this.classifier.equals(other.classifier) && this.type.equals(other.type) && Objects.equals(this.filename, other.filename);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return getKey();
+ }
+
+ private static String notNull(String value) {
+
+ if (value == null) {
+ return "";
+ }
+ return value;
+ }
+
+ private static String requireNotEmpty(String value, String propertyName) {
+
+ if (isEmpty(value)) {
+ throw new IllegalArgumentException("Maven artifact property " + propertyName + " must not be empty");
+ }
+ return value;
+ }
+
+ private static boolean isEmpty(String value) {
+
+ return ((value == null) || value.isEmpty());
+ }
+
+ /**
+ * @param artifactId the {@link #getArtifactId() artifact ID}.
+ * @param version the {@link #getVersion() version}.
+ * @param type the {@link #getType() type}.
+ * @param classifier the {@link #getClassifier() classifier}.
+ * @return the IDEasy {@link MvnArtifact}.
+ */
+ public static MvnArtifact ofIdeasy(String artifactId, String version, String type, String classifier) {
+
+ return new MvnArtifact(GROUP_ID_IDEASY, artifactId, version, type, classifier);
+ }
+
+ /**
+ * @param version the {@link #getVersion() version}.
+ * @param type the {@link #getType() type}.
+ * @param classifier the {@link #getClassifier() classifier}.
+ * @return the IDEasy {@link MvnArtifact}.
+ */
+ public static MvnArtifact ofIdeasyCli(String version, String type, String classifier) {
+
+ return ofIdeasy(ARTIFACT_ID_IDEASY_CLI, version, type, classifier);
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java b/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java
index 4de72f2ec..a22fed4c1 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java
@@ -103,14 +103,24 @@ private List computeSortedVersions(String tool, String editio
* @return the latest matching {@link VersionIdentifier} for the given {@code tool} and {@code edition}.
*/
public VersionIdentifier getVersion(String tool, String edition, GenericVersionRange version) {
+ List versions = getSortedVersions(tool, edition);
+ return resolveVersionPattern(version, versions);
+ }
+ /**
+ * Resolves a version pattern against a list of available versions.
+ *
+ * @param version the version pattern to resolve
+ * @param versions the available versions, sorted in descending order
+ * @return the resolved version
+ */
+ public VersionIdentifier resolveVersionPattern(GenericVersionRange version, List versions) {
if (version == null) {
version = VersionIdentifier.LATEST;
}
if (!version.isPattern()) {
return (VersionIdentifier) version;
}
- List versions = getSortedVersions(tool, edition);
for (VersionIdentifier vi : versions) {
if (version.contains(vi)) {
this.context.debug("Resolved version pattern {} to version {}", version, vi);
@@ -118,8 +128,7 @@ public VersionIdentifier getVersion(String tool, String edition, GenericVersionR
}
}
throw new CliException(
- "Could not find any version matching '" + version + "' for tool '" + tool + "' - potentially there are " + versions.size() + " version(s) available in "
- + getEdition(tool, edition).getPath() + " but none matched!");
+ "Could not find any version matching '" + version + "' - there are " + versions.size() + " version(s) available but none matched!");
}
/**
diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/IdeVersion.java b/cli/src/main/java/com/devonfw/tools/ide/version/IdeVersion.java
index db7bd0f30..7051f9273 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/version/IdeVersion.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/version/IdeVersion.java
@@ -8,6 +8,9 @@
*/
public final class IdeVersion {
+ /** The fallback version used if the version is undefined (in local development). */
+ public static final String VERSION_UNDEFINED = "SNAPSHOT";
+
private static final IdeVersion INSTANCE = new IdeVersion();
private final String version;
@@ -21,7 +24,7 @@ private IdeVersion() {
super();
String v = getClass().getPackage().getImplementationVersion();
if (v == null) {
- v = "SNAPSHOT";
+ v = VERSION_UNDEFINED;
}
this.version = v;
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/VersionIdentifier.java b/cli/src/main/java/com/devonfw/tools/ide/version/VersionIdentifier.java
index 7683754bd..01f5f8d27 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/version/VersionIdentifier.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/version/VersionIdentifier.java
@@ -11,6 +11,9 @@ public final class VersionIdentifier implements VersionObject
/** {@link VersionIdentifier} "*" that will resolve to the latest stable version. */
public static final VersionIdentifier LATEST = VersionIdentifier.of("*");
+ /** {@link VersionIdentifier} "*!" that will resolve to the latest snapshot. */
+ public static final VersionIdentifier LATEST_UNSTABLE = VersionIdentifier.of("*!");
+
private final VersionSegment start;
private final VersionLetters developmentPhase;
diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java b/cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java
index 26246e016..36e78a051 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java
@@ -85,12 +85,12 @@ public String getSeparator() {
/**
* @return the letters or the empty {@link String} ("") for none. In canonical {@link VersionIdentifier}s letters indicate the development phase (e.g. "pre",
- * "rc", "alpha", "beta", "milestone", "test", "dev", "SNAPSHOT", etc.). However, letters are technically any
- * {@link Character#isLetter(char) letter characters} and may also be something like a code-name (e.g. "Cupcake", "Donut", "Eclair", "Froyo", "Gingerbread",
- * "Honeycomb", "Ice Cream Sandwich", "Jelly Bean" in case of Android internals). Please note that in such case it is impossible to properly decide which
- * version is greater than another versions. To avoid mistakes, the comparison supports a strict mode that will let the comparison fail in such case. However,
- * by default (e.g. for {@link Comparable#compareTo(Object)}) the default {@link String#compareTo(String) string comparison} (lexicographical) is used to
- * ensure a natural order.
+ * "rc", "alpha", "beta", "milestone", "test", "dev", "SNAPSHOT", etc.). However, letters are technically any
+ * {@link Character#isLetter(char) letter characters} and may also be something like a code-name (e.g. "Cupcake", "Donut", "Eclair", "Froyo",
+ * "Gingerbread", "Honeycomb", "Ice Cream Sandwich", "Jelly Bean" in case of Android internals). Please note that in such case it is impossible to
+ * properly decide which version is greater than another versions. To avoid mistakes, the comparison supports a strict mode that will let the comparison
+ * fail in such case. However, by default (e.g. for {@link Comparable#compareTo(Object)}) the default {@link String#compareTo(String) string comparison}
+ * (lexicographical) is used to ensure a natural order.
* @see #getPhase()
*/
public String getLettersString() {
@@ -108,7 +108,7 @@ public VersionLetters getLetters() {
/**
* @return the {@link VersionPhase} for the {@link #getLettersString() letters}. Will be {@link VersionPhase#UNDEFINED} if unknown and hence never
- * {@code null}.
+ * {@code null}.
* @see #getLettersString()
*/
public VersionPhase getPhase() {
@@ -118,8 +118,8 @@ public VersionPhase getPhase() {
/**
* @return the digits or the empty {@link String} ("") for none. This is the actual {@link #getNumber() number} part of this {@link VersionSegment}. So the
- * {@link VersionIdentifier} "1.0.001" will have three segments: The first one with "1" as digits, the second with "0" as digits, and a third with "001" as
- * digits. You can get the same value via {@link #getNumber()} but this {@link String} representation will preserve leading zeros.
+ * {@link VersionIdentifier} "1.0.001" will have three segments: The first one with "1" as digits, the second with "0" as digits, and a third with "001"
+ * as digits. You can get the same value via {@link #getNumber()} but this {@link String} representation will preserve leading zeros.
*/
public String getDigits() {
@@ -136,7 +136,7 @@ public int getNumber() {
/**
* @return the potential pattern that is {@link #PATTERN_MATCH_ANY_STABLE_VERSION}, {@link #PATTERN_MATCH_ANY_VERSION}, or for no pattern the empty
- * {@link String}.
+ * {@link String}.
*/
public String getPattern() {
@@ -292,9 +292,9 @@ public VersionMatchResult matches(VersionSegment other) {
/**
* @return the {@link VersionLetters} that represent a {@link VersionLetters#isDevelopmentPhase() development phase} searching from this
- * {@link VersionSegment} to all {@link #getNextOrNull() next segments}. Will be {@link VersionPhase#NONE} if no
- * {@link VersionPhase#isDevelopmentPhase() development phase} was found and {@link VersionPhase#UNDEFINED} if multiple
- * {@link VersionPhase#isDevelopmentPhase() development phase}s have been found.
+ * {@link VersionSegment} to all {@link #getNextOrNull() next segments}. Will be {@link VersionPhase#NONE} if no
+ * {@link VersionPhase#isDevelopmentPhase() development phase} was found and {@link VersionPhase#UNDEFINED} if multiple
+ * {@link VersionPhase#isDevelopmentPhase() development phase}s have been found.
* @see VersionIdentifier#getDevelopmentPhase()
*/
protected VersionLetters getDevelopmentPhase() {
@@ -361,9 +361,6 @@ static VersionSegment of(String version) {
if (current == null) {
start = segment;
} else {
- if (!current.getPattern().isEmpty()) {
- throw new IllegalArgumentException("Invalid version pattern: " + version);
- }
current.next = segment;
}
current = segment;
diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties
index ccdbaa1b1..5cac9aed8 100644
--- a/cli/src/main/resources/nls/Help.properties
+++ b/cli/src/main/resources/nls/Help.properties
@@ -108,8 +108,10 @@ cmd.uninstall-plugin.detail=Plugins can be only installed or uninstalled for too
cmd.uninstall.detail=Can be used to uninstall any tool e.g. to uninstall java simply type: 'uninstall java'.
cmd.update=Pull your settings and apply updates (software, configuration and repositories).
cmd.update.detail=To update your IDE (if instructed by your ide-admin), you only need to run the following command: 'ide update'.
+cmd.upgrade=Upgrade the version of IDEasy to the latest version available.
cmd.upgrade-settings=Commandlet to upgrade settings of a devonfw-ide project, to allow migration to IDEasy.
cmd.upgrade-settings.detail=Renames and reconfigures all devon.properties, replaces all legacy variables, updates folder names and points out all xml files that are not compatible for the xml merger.
+cmd.upgrade.detail=Automatically checks for and installs the latest available version. If using a Snapshot version of IDEasy, this command will install the latest available Snapshot.
cmd.version=Print the version of IDEasy.
cmd.version.detail=To print the current version of IDEasy simply type: 'ide --version'.
cmd.vscode=Tool commandlet for Visual Studio Code (IDE).
diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties
index 6abc331c4..3774bd022 100644
--- a/cli/src/main/resources/nls/Help_de.properties
+++ b/cli/src/main/resources/nls/Help_de.properties
@@ -108,8 +108,10 @@ cmd.uninstall-plugin.detail=Erweiterung können nur für Werkzeuge installiert u
cmd.uninstall.detail=Wird dazu verwendet um jedwedes Werkzeug zu deinstallieren. Um z.B. Java zu deinstallieren geben Sie einfach 'uninstall java' in die Konsole ein.
cmd.update=Updatet die Settings, Software und Repositories.
cmd.update.detail=Um die IDE auf den neuesten Stand zu bringen (falls von Ihrem Admin angewiesen) geben Sie einfach 'ide update' in die Konsole ein.
+cmd.upgrade=Aktualisiere IDEasy auf die neueste Version.
cmd.upgrade-settings=Kommando zum Aufwerten der Einstellungen eines devonfw-ide Projekts, um den Umstieg zu IDEasy zu ermöglichen.
cmd.upgrade-settings.detail=Benennt alle devon.properties um und konfiguriert sie neu, ersetzt alle legacy Variablen, aktualisiert Ordnernamen und weist auf alle XML-Dateien hin, die nicht für den xml merger kompatibel sind.
+cmd.upgrade.detail=Prüft automatisch auf neue Versionen und installiert die neueste verfügbare Version. Bei Verwendung einer Snapshot-Version von IDEasy wird der neueste verfügbare Snapshot installiert.
cmd.version=Gibt die Version von IDEasy aus.
cmd.version.detail=Um die aktuelle Version von IDEasy auszugeben, brauchen Sie einfach nur 'ide --version' in die Konsole eingeben.
cmd.vscode=Werkzeug Kommando für Visual Studio Code (IDE).
diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpgradeCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpgradeCommandletTest.java
new file mode 100644
index 000000000..9b5b8ef4e
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpgradeCommandletTest.java
@@ -0,0 +1,22 @@
+package com.devonfw.tools.ide.commandlet;
+
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.IdeTestContext;
+import org.junit.jupiter.api.Test;
+
+class UpgradeCommandletTest extends AbstractIdeContextTest {
+
+ @Test
+ public void testSnapshotVersionComparisons() {
+
+ IdeTestContext context = newContext(PROJECT_BASIC);
+ UpgradeCommandlet uc = context.getCommandletManager().getCommandlet(UpgradeCommandlet.class);
+
+ assertThat(uc.isSnapshotNewer("2024.12.002-beta-12_18_02-SNAPSHOT", "2025.01.001-beta-20250118.022832-8")).isTrue();
+ assertThat(uc.isSnapshotNewer("2024.12.002-beta-01_01_02-SNAPSHOT", "2024.12.002-beta-20241218.023429-8")).isTrue();
+ assertThat(
+ uc.isSnapshotNewer("2024.12.002-beta-12_18_02-SNAPSHOT", "2024.12.002-beta-20241218.023429-8")).isFalse();
+ assertThat(uc.isSnapshotNewer("SNAPSHOT", "2024.12.002-beta-20241218.023429-8")).isFalse();
+ assertThat(uc.isSnapshotNewer("someUnknownFormat1", "someUnknownFormat2")).isFalse();
+ }
+}
\ No newline at end of file
diff --git a/cli/src/test/java/com/devonfw/tools/ide/repo/MavenRepositoryTest.java b/cli/src/test/java/com/devonfw/tools/ide/repo/MavenRepositoryTest.java
new file mode 100644
index 000000000..b3b77d86c
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/repo/MavenRepositoryTest.java
@@ -0,0 +1,54 @@
+package com.devonfw.tools.ide.repo;
+
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.IdeTestContext;
+import com.devonfw.tools.ide.os.OperatingSystem;
+import com.devonfw.tools.ide.os.SystemArchitecture;
+import com.devonfw.tools.ide.url.model.file.UrlDownloadFileMetadata;
+import com.devonfw.tools.ide.version.VersionIdentifier;
+import org.junit.jupiter.api.Test;
+
+class MavenRepositoryTest extends AbstractIdeContextTest {
+
+ @Test
+ void testGetMetadataWithRelease() {
+
+ // arrange
+ IdeTestContext context = newContext(PROJECT_BASIC);
+ MavenRepository mavenRepo = new MavenRepository(context);
+ String tool = "ideasy";
+ String edition = tool;
+ VersionIdentifier version = VersionIdentifier.of("2024.04.001-beta");
+ OperatingSystem os = context.getSystemInfo().getOs();
+ SystemArchitecture arch = context.getSystemInfo().getArchitecture();
+
+ // act
+ UrlDownloadFileMetadata metadata = mavenRepo.getMetadata(tool, edition, version);
+
+ // assert
+ assertThat(metadata.getUrls()).containsExactly(
+ "https://repo1.maven.org/maven2/com/devonfw/tools/IDEasy/ide-cli/2024.04.001-beta/ide-cli-2024.04.001-beta-" + os + "-" + arch + ".tar.gz");
+ }
+
+ @Test
+ void testGetMetadataWithSnapshot() {
+
+ // arrange
+ IdeTestContext context = newContext(PROJECT_BASIC);
+ MavenRepository mavenRepo = new MavenRepository(context);
+ String tool = "ideasy";
+ String edition = tool;
+ VersionIdentifier version = VersionIdentifier.of("2024.04.001-beta-20240419.123456-1");
+ OperatingSystem os = context.getSystemInfo().getOs();
+ SystemArchitecture arch = context.getSystemInfo().getArchitecture();
+
+ // act
+ UrlDownloadFileMetadata metadata = mavenRepo.getMetadata(tool, edition, version);
+
+ // assert
+ assertThat(metadata.getUrls()).containsExactly(
+ "https://s01.oss.sonatype.org/content/repositories/snapshots/com/devonfw/tools/IDEasy/ide-cli/2024.04.001-beta-SNAPSHOT/ide-cli-2024.04.001-beta-20240419.123456-1-"
+ + os + "-" + arch + ".tar.gz");
+ }
+
+}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/mvn/MvnArtifactTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/mvn/MvnArtifactTest.java
new file mode 100644
index 000000000..45fa351da
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/tool/mvn/MvnArtifactTest.java
@@ -0,0 +1,126 @@
+package com.devonfw.tools.ide.tool.mvn;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test of {@link MvnArtifact}.
+ */
+public class MvnArtifactTest extends Assertions {
+
+ /**
+ * Test of {@link MvnArtifact#ofIdeasyCli(String, String, String)}.
+ */
+ @Test
+ public void testIdeasyCli() {
+
+ // arrange
+ String version = "2025.12.001";
+ String type = "tar.gz";
+ String classifier = "windows-arm64";
+ // act
+ MvnArtifact artifact = MvnArtifact.ofIdeasyCli(version, type, classifier);
+ MvnArtifact equal = new MvnArtifact(MvnArtifact.GROUP_ID_IDEASY, MvnArtifact.ARTIFACT_ID_IDEASY_CLI, version, type, classifier);
+ // assert
+ assertThat(artifact.getGroupId()).isEqualTo("com.devonfw.tools.IDEasy");
+ assertThat(artifact.getArtifactId()).isEqualTo("ide-cli");
+ assertThat(artifact.getVersion()).isEqualTo(version);
+ assertThat(artifact.getType()).isEqualTo(type);
+ assertThat(artifact.getClassifier()).isEqualTo(classifier);
+ assertThat(artifact.getPath()).isEqualTo("com/devonfw/tools/IDEasy/ide-cli/2025.12.001/ide-cli-2025.12.001-windows-arm64.tar.gz");
+ assertThat(artifact).hasToString("com.devonfw.tools.IDEasy:ide-cli:2025.12.001:tar.gz:windows-arm64");
+ assertThat(artifact.getKey()).isEqualTo(artifact.toString());
+ assertThat(artifact.getDownloadUrl()).isEqualTo(
+ "https://repo1.maven.org/maven2/com/devonfw/tools/IDEasy/ide-cli/2025.12.001/ide-cli-2025.12.001-windows-arm64.tar.gz");
+ assertThat(artifact).isEqualTo(equal);
+ assertThat(artifact.hashCode()).isEqualTo(equal.hashCode());
+ }
+
+ /**
+ * Test of {@link MvnArtifact#ofIdeasyCli(String, String, String)} with SNAPSHOT version.
+ */
+ @Test
+ public void testIdeasyCliSnapshot() {
+
+ // arrange
+ String version = "2025.01.003-beta-20250130.023001-3";
+ String type = "tar.gz";
+ String classifier = "windows-x64";
+ // act
+ MvnArtifact artifact = MvnArtifact.ofIdeasyCli(version, type, classifier);
+ MvnArtifact equal = new MvnArtifact(MvnArtifact.GROUP_ID_IDEASY, MvnArtifact.ARTIFACT_ID_IDEASY_CLI, version, type, classifier);
+ // assert
+ assertThat(artifact.getGroupId()).isEqualTo("com.devonfw.tools.IDEasy");
+ assertThat(artifact.getArtifactId()).isEqualTo("ide-cli");
+ assertThat(artifact.getVersion()).isEqualTo(version);
+ assertThat(artifact.getType()).isEqualTo(type);
+ assertThat(artifact.getClassifier()).isEqualTo(classifier);
+ assertThat(artifact.getPath()).isEqualTo(
+ "com/devonfw/tools/IDEasy/ide-cli/2025.01.003-beta-SNAPSHOT/ide-cli-2025.01.003-beta-20250130.023001-3-windows-x64.tar.gz");
+ assertThat(artifact).hasToString("com.devonfw.tools.IDEasy:ide-cli:2025.01.003-beta-20250130.023001-3:tar.gz:windows-x64");
+ assertThat(artifact.getDownloadUrl()).isEqualTo(
+ "https://s01.oss.sonatype.org/content/repositories/snapshots/com/devonfw/tools/IDEasy/ide-cli/2025.01.003-beta-SNAPSHOT/ide-cli-2025.01.003-beta-20250130.023001-3-windows-x64.tar.gz");
+ assertThat(artifact.getKey()).isEqualTo(artifact.toString());
+ assertThat(artifact).isEqualTo(equal);
+ assertThat(artifact.hashCode()).isEqualTo(equal.hashCode());
+ }
+
+ /**
+ * Test of {@link MvnArtifact#withMavenMetadata()}.
+ */
+ @Test
+ public void testMetadata() {
+
+ // arrange
+ String groupId = "org.apache.maven.plugins";
+ String artifactId = "maven-clean-plugin";
+ String version = "*";
+ String type = "xml";
+ // act
+ MvnArtifact artifact = new MvnArtifact(groupId, artifactId, version).withMavenMetadata();
+ MvnArtifact equal = new MvnArtifact(groupId, artifactId, version, type, "", MvnArtifact.MAVEN_METADATA_XML);
+ // assert
+ assertThat(artifact.getGroupId()).isEqualTo(groupId);
+ assertThat(artifact.getArtifactId()).isEqualTo(artifactId);
+ assertThat(artifact.getVersion()).isEqualTo(version);
+ assertThat(artifact.getType()).isEqualTo(type);
+ assertThat(artifact.getClassifier()).isEmpty();
+ assertThat(artifact.getPath()).isEqualTo("org/apache/maven/plugins/maven-clean-plugin/maven-metadata.xml");
+ assertThat(artifact).hasToString("org.apache.maven.plugins:maven-clean-plugin:*:xml");
+ assertThat(artifact.getKey()).isEqualTo(artifact.toString());
+ assertThat(artifact.getDownloadUrl()).isEqualTo(
+ "https://repo1.maven.org/maven2/org/apache/maven/plugins/maven-clean-plugin/maven-metadata.xml");
+ assertThat(artifact).isEqualTo(equal);
+ assertThat(artifact.hashCode()).isEqualTo(equal.hashCode());
+ }
+
+ /**
+ * Test of {@link MvnArtifact#withMavenMetadata()}.
+ */
+ @Test
+ public void testMetadataWithSnapshot() {
+
+ // arrange
+ String groupId = "org.apache.maven.plugins";
+ String artifactId = "maven-clean-plugin";
+ String version = "*-SNAPSHOT";
+ String type = "xml";
+ // act
+ MvnArtifact artifact = new MvnArtifact(groupId, artifactId, version).withMavenMetadata();
+ MvnArtifact equal = new MvnArtifact(groupId, artifactId, version, type, "", MvnArtifact.MAVEN_METADATA_XML);
+ // assert
+ assertThat(artifact.getGroupId()).isEqualTo(groupId);
+ assertThat(artifact.getArtifactId()).isEqualTo(artifactId);
+ assertThat(artifact.getVersion()).isEqualTo(version);
+ assertThat(artifact.getType()).isEqualTo(type);
+ assertThat(artifact.getClassifier()).isEmpty();
+ assertThat(artifact.getPath()).isEqualTo("org/apache/maven/plugins/maven-clean-plugin/maven-metadata.xml");
+ assertThat(artifact).hasToString("org.apache.maven.plugins:maven-clean-plugin:*-SNAPSHOT:xml");
+ assertThat(artifact.getKey()).isEqualTo(artifact.toString());
+ assertThat(artifact.getDownloadUrl()).isEqualTo(
+ "https://s01.oss.sonatype.org/content/repositories/snapshots/org/apache/maven/plugins/maven-clean-plugin/maven-metadata.xml");
+ assertThat(artifact).isEqualTo(equal);
+ assertThat(artifact.hashCode()).isEqualTo(equal.hashCode());
+ }
+
+}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/version/VersionIdentifierTest.java b/cli/src/test/java/com/devonfw/tools/ide/version/VersionIdentifierTest.java
index 6ffedfada..c048e19ca 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/version/VersionIdentifierTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/version/VersionIdentifierTest.java
@@ -77,7 +77,8 @@ public void testValid(String version) {
*/
@ParameterizedTest
// arrange
- @ValueSource(strings = { "0", "0.0", "1.0.pineapple-pen", "1.0-rc", ".1.0", "1.-0", "RC1", "Beta1", "donut", "8u412b08" })
+ @ValueSource(strings = { "0", "0.0", "1.0.pineapple-pen", "1.0-rc", ".1.0", "1.-0", "RC1", "Beta1", "donut", "8u412b08", "0*.0", "*0", "*.", "17.*alpha",
+ "17*.1" })
public void testInvalid(String version) {
// act
@@ -85,28 +86,9 @@ public void testInvalid(String version) {
// assert
assertThat(vid.isValid()).as(version).isFalse();
- assertThat(vid.isPattern()).isFalse();
assertThat(vid).hasToString(version);
}
- /**
- * Test of illegal versions.
- */
- @Test
- public void testIllegal() {
-
- String[] illegalVersions = { "0*.0", "*0", "*.", "17.*alpha", "17*.1" };
- for (String version : illegalVersions) {
- try {
- VersionIdentifier.of(version);
- fail("Illegal version '" + version + "' did not cause an exception!");
- } catch (Exception e) {
- assertThat(e).isInstanceOf(IllegalArgumentException.class);
- assertThat(e).hasMessageContaining(version);
- }
- }
- }
-
/**
* Test of {@link VersionIdentifier} with canonical version numbers and safe order.
*/
@@ -167,6 +149,7 @@ public void testMatchStable() {
assertThat(pattern.matches(VersionIdentifier.of("17.alpha7"))).isFalse();
assertThat(pattern.matches(VersionIdentifier.of("17.beta2"))).isFalse();
assertThat(pattern.matches(VersionIdentifier.of("17-SNAPSHOT"))).isFalse();
+ assertThat(pattern.matches(VersionIdentifier.of("18.0"))).isFalse();
pattern = VersionIdentifier.of("17.*");
assertThat(pattern.isValid()).isFalse();
assertThat(pattern.isPattern()).isTrue();