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();