diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 84904ec6e..e3f264538 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,7 +6,10 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/757[#757]: Support to allow settings in code repository +* https://github.com/devonfw/IDEasy/issues/826[#826]: Fix git settings check when settings folder is empty * https://github.com/devonfw/IDEasy/issues/894[#894]: Fix ide.bat printing for initialization and error output +* https://github.com/devonfw/IDEasy/issues/759[#759]: Add UpgradeSettingsCommandlet for the upgrade of legacy devonfw-ide settings to IDEasy The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/18?closed=1[milestone 2025.01.001]. diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 78df57524..60b7b0ab6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -12,7 +12,7 @@ import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.property.StringProperty; -import com.devonfw.tools.ide.repo.CustomTool; +import com.devonfw.tools.ide.repo.CustomToolMetadata; import com.devonfw.tools.ide.step.Step; import com.devonfw.tools.ide.tool.CustomToolCommandlet; import com.devonfw.tools.ide.tool.ToolCommandlet; @@ -106,7 +106,11 @@ private void setupConf(Path template, Path conf) { } } - private void updateSettings() { + /** + * Updates the settings repository in IDE_HOME/settings by either cloning if no such repository exists or pulling + * if the repository exists then saves the latest current commit ID in the file ".commit.id". + */ + protected void updateSettings() { Path settingsPath = this.context.getSettingsPath(); GitContext gitContext = this.context.getGitContext(); @@ -132,6 +136,7 @@ private void updateSettings() { } gitContext.pullOrClone(GitUrl.of(repository), settingsPath); } + this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); step.success("Successfully updated settings repository."); } finally { if (step != null) { @@ -164,7 +169,7 @@ private void updateSoftware() { } // custom tools in ide-custom-tools.json - for (CustomTool customTool : this.context.getCustomToolRepository().getTools()) { + for (CustomToolMetadata customTool : this.context.getCustomToolRepository().getTools()) { CustomToolCommandlet customToolCommandlet = new CustomToolCommandlet(this.context, customTool); toolCommandlets.add(customToolCommandlet); } 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 7442bcbfc..8103b3ae3 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 @@ -88,6 +88,7 @@ public CommandletManagerImpl(IdeContext context) { add(new RepositoryCommandlet(context)); add(new UninstallCommandlet(context)); add(new UpdateCommandlet(context)); + add(new UpgradeSettingsCommandlet(context)); add(new CreateCommandlet(context)); add(new BuildCommandlet(context)); add(new InstallPluginCommandlet(context)); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index 4dc07641b..f065d614f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -1,8 +1,10 @@ package com.devonfw.tools.ide.commandlet; +import java.nio.file.Files; import java.nio.file.Path; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.property.StringProperty; @@ -18,6 +20,9 @@ public class CreateCommandlet extends AbstractUpdateCommandlet { /** {@link FlagProperty} for skipping the setup of git repositories */ public final FlagProperty skipRepositories; + /** {@link FlagProperty} for creating a project with settings inside a code repository */ + public final FlagProperty codeRepositoryFlag; + /** * The constructor. * @@ -28,6 +33,7 @@ public CreateCommandlet(IdeContext context) { super(context); this.newProject = add(new StringProperty("", true, "project")); this.skipRepositories = add(new FlagProperty("--skip-repositories")); + this.codeRepositoryFlag = add(new FlagProperty("--code")); add(this.settingsRepo); } @@ -59,12 +65,32 @@ public void run() { initializeProject(newProjectPath); this.context.setIdeHome(newProjectPath); super.run(); + if (this.skipRepositories.isTrue()) { this.context.info("Skipping the cloning of project repositories as specified by the user."); } else { updateRepositories(); } this.context.success("Successfully created new project '{}'.", newProjectName); + + } + + private void initializeCodeRepository(String repoUrl) { + + // clone the given repository into IDE_HOME/workspaces/main + GitUrl gitUrl = GitUrl.of(repoUrl); + Path codeRepoPath = this.context.getWorkspacePath().resolve(gitUrl.getProjectName()); + this.context.getGitContext().pullOrClone(gitUrl, codeRepoPath); + + // check for settings folder and create symlink to IDE_HOME/settings + Path settingsFolder = codeRepoPath.resolve(IdeContext.FOLDER_SETTINGS); + if (Files.exists(settingsFolder)) { + this.context.getFileAccess().symlink(settingsFolder, this.context.getSettingsPath()); + // create a file in IDE_HOME with the current local commit id + this.context.getGitContext().saveCurrentCommitId(codeRepoPath, this.context.getSettingsCommitIdPath()); + } else { + this.context.warning("No settings folder was found inside the code repository."); + } } private void initializeProject(Path newInstancePath) { @@ -79,4 +105,25 @@ private void updateRepositories() { this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class).run(); } + + @Override + protected void updateSettings() { + + if (codeRepositoryFlag.isTrue()) { + String codeRepository = this.settingsRepo.getValue(); + if (codeRepository == null || codeRepository.isBlank()) { + String message = """ + No code repository was given after '--code'. + Please give the code repository below that includes your settings folder. + Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc + Code repository URL: + """; + codeRepository = this.context.askForInput(message); + } + initializeCodeRepository(codeRepository); + } else { + super.updateSettings(); + } + + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java index b7daa7e17..5b61b7a58 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java @@ -57,7 +57,8 @@ public void run() { return; } - List propertiesFiles = this.context.getFileAccess().listChildren(repositories, path -> path.getFileName().toString().endsWith(".properties")); + List propertiesFiles = this.context.getFileAccess() + .listChildren(repositories, path -> path.getFileName().toString().endsWith(".properties")); boolean forceMode = this.context.isForceMode(); for (Path propertiesFile : propertiesFiles) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java index b706d944a..94d4401e4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java @@ -55,10 +55,10 @@ private void logSettingsLegacyStatus() { } private void logSettingsGitStatus() { - Path settingsPath = this.context.getSettingsPath(); + Path settingsPath = this.context.getSettingsGitRepository(); if (settingsPath != null) { GitContext gitContext = this.context.getGitContext(); - if (gitContext.isRepositoryUpdateAvailable(settingsPath)) { + if (gitContext.isRepositoryUpdateAvailable(settingsPath, this.context.getSettingsCommitIdPath())) { this.context.warning("Your settings are not up-to-date, please run 'ide update'."); } else { this.context.success("Your settings are up-to-date."); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpgradeSettingsCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpgradeSettingsCommandlet.java new file mode 100644 index 000000000..4eed38112 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpgradeSettingsCommandlet.java @@ -0,0 +1,183 @@ +package com.devonfw.tools.ide.commandlet; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.function.Function; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.environment.EnvironmentVariablesPropertiesFile; +import com.devonfw.tools.ide.environment.EnvironmentVariablesType; +import com.devonfw.tools.ide.merge.DirectoryMerger; +import com.devonfw.tools.ide.repo.CustomToolsJson; +import com.devonfw.tools.ide.repo.CustomToolsJsonMapper; +import com.devonfw.tools.ide.tool.mvn.Mvn; +import com.devonfw.tools.ide.variable.IdeVariables; +import com.devonfw.tools.ide.variable.VariableDefinition; + +/** + * {@link Commandlet} to upgrade settings after a migration from devonfw-ide to IDEasy. + */ +public class UpgradeSettingsCommandlet extends Commandlet { + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public UpgradeSettingsCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + } + + @Override + public String getName() { + + return "upgrade-settings"; + } + + @Override + public void run() { + updateLegacyFolders(); + updateProperties(); + updateWorkspaceTemplates(); + } + + private void updateLegacyFolders() { + this.context.info("Updating legacy folders if present..."); + Path settingsPath = context.getSettingsPath(); + updateLegacyFolder(settingsPath, IdeContext.FOLDER_LEGACY_REPOSITORIES, IdeContext.FOLDER_REPOSITORIES); + updateLegacyFolder(settingsPath, IdeContext.FOLDER_LEGACY_TEMPLATES, IdeContext.FOLDER_TEMPLATES); + updateLegacyFolder(settingsPath.resolve(IdeContext.FOLDER_TEMPLATES).resolve(IdeContext.FOLDER_CONF), Mvn.MVN_CONFIG_LEGACY_FOLDER, Mvn.MVN_CONFIG_FOLDER); + } + + private void updateLegacyFolder(Path folder, String legacyName, String newName) { + + Path legacyFolder = folder.resolve(legacyName); + Path newFolder = folder.resolve(newName); + if (Files.isDirectory(legacyFolder)) { + try { + if (!Files.exists(newFolder)) { + Files.move(legacyFolder, newFolder, StandardCopyOption.REPLACE_EXISTING); + this.context.success("Successfully renamed folder '{}' to '{}' in {}.", legacyName, newName, folder); + } + } catch (IOException e) { + this.context.error(e, "Error renaming folder {} to {} in {}", legacyName, newName, folder); + } + } + } + + private void updateWorkspaceTemplates() { + this.context.info("Updating workspace templates (replace legacy variables and change variable syntax)..."); + + DirectoryMerger merger = this.context.getWorkspaceMerger(); + Path settingsDir = this.context.getSettingsPath(); + Path workspaceDir = settingsDir.resolve(IdeContext.FOLDER_WORKSPACE); + if (Files.isDirectory(workspaceDir)) { + merger.upgrade(workspaceDir); + } + this.context.getFileAccess().listChildrenMapped(settingsDir, child -> { + Path childWorkspaceDir = child.resolve(IdeContext.FOLDER_WORKSPACE); + if (Files.isDirectory(childWorkspaceDir)) { + merger.upgrade(childWorkspaceDir); + } + return null; + }); + } + + private void updateProperties() { + // updates DEVON_IDE_CUSTOM_TOOLS to new ide-custom-tools.json + String devonCustomTools = IdeVariables.DEVON_IDE_CUSTOM_TOOLS.get(this.context); + if (devonCustomTools != null) { + CustomToolsJson customToolsJson = CustomToolsJsonMapper.parseCustomToolsFromLegacyConfig(devonCustomTools, context); + if (customToolsJson != null) { + CustomToolsJsonMapper.saveJson(customToolsJson, this.context.getSettingsPath().resolve(IdeContext.FILE_CUSTOM_TOOLS)); + } + } + + // update properties (devon.properties -> ide.properties, convert legacy properties) + EnvironmentVariables environmentVariables = context.getVariables(); + while (environmentVariables != null) { + if (environmentVariables instanceof EnvironmentVariablesPropertiesFile environmentVariablesProperties) { + updateProperties(environmentVariablesProperties); + } + environmentVariables = environmentVariables.getParent(); + } + Path templatePropertiesDir = this.context.getSettingsTemplatePath().resolve(IdeContext.FOLDER_CONF); + if (Files.exists(templatePropertiesDir)) { + EnvironmentVariablesPropertiesFile environmentVariablesProperties = new EnvironmentVariablesPropertiesFile(null, EnvironmentVariablesType.CONF, + templatePropertiesDir, null, this.context); + updateProperties(environmentVariablesProperties); + } + } + + private void updateProperties(EnvironmentVariablesPropertiesFile environmentVariables) { + Path propertiesFilePath = environmentVariables.getPropertiesFilePath(); + if (environmentVariables.getLegacyConfiguration() != null) { + if (environmentVariables.getType() == EnvironmentVariablesType.SETTINGS) { + // adds disabled legacySupportEnabled variable if missing in ide.properties + environmentVariables.set(IdeVariables.IDE_VARIABLE_SYNTAX_LEGACY_SUPPORT_ENABLED.getName(), "false", false); + } + environmentVariables.remove(IdeVariables.DEVON_IDE_CUSTOM_TOOLS.getName()); + for (VariableDefinition var : IdeVariables.VARIABLES) { + String legacyName = var.getLegacyName(); + if (legacyName != null) { + String value = environmentVariables.get(legacyName); + if (value != null) { + String name = var.getName(); + String newValue = environmentVariables.get(name); + if (newValue == null) { + environmentVariables.set(name, value, environmentVariables.isExported(name)); + } + } + environmentVariables.remove(legacyName); + } + } + updatePropertiesLegacyEdition(environmentVariables, "INTELLIJ_EDITION_TYPE", "INTELLIJ_EDITION", this::mapLegacyIntellijEdition); + updatePropertiesLegacyEdition(environmentVariables, "ECLIPSE_EDITION_TYPE", "ECLIPSE_EDITION", this::mapLegacyEclipseEdition); + environmentVariables.save(); + this.context.getFileAccess().backup(environmentVariables.getLegacyPropertiesFilePath()); + } + } + + private String mapLegacyIntellijEdition(String legacyEdition) { + + return switch (legacyEdition) { + case "U" -> "ultimate"; + case "C" -> "intellij"; + default -> { + this.context.warning("Undefined legacy edition {}", legacyEdition); + yield "intellij"; + } + }; + } + + private String mapLegacyEclipseEdition(String legacyEdition) { + + return switch (legacyEdition) { + case "java" -> "eclipse"; + case "jee" -> "jee"; + case "cpp" -> "cpp"; + default -> { + this.context.warning("Undefined legacy edition {}", legacyEdition); + yield "eclipse"; + } + }; + } + + private static void updatePropertiesLegacyEdition(EnvironmentVariablesPropertiesFile environmentVariables, String legacyEditionVariable, + String newEditionVariable, Function editionMapper) { + + String legacyEdition = environmentVariables.get(legacyEditionVariable); + if (legacyEdition != null) { + String newEdition = environmentVariables.get(newEditionVariable); + if (newEdition == null) { + environmentVariables.set(newEditionVariable, editionMapper.apply(legacyEdition), false); + } + environmentVariables.remove(legacyEditionVariable); + } + } +} 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 eb5e16e46..f571c4f3d 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 @@ -77,11 +77,13 @@ public abstract class AbstractIdeContext implements IdeContext { protected Path settingsPath; + private Path settingsCommitIdPath; + private Path softwarePath; private Path softwareExtraPath; - private Path softwareRepositoryPath; + private final Path softwareRepositoryPath; protected Path pluginsPath; @@ -91,15 +93,15 @@ public abstract class AbstractIdeContext implements IdeContext { protected Path urlsPath; - private Path tempPath; + private final Path tempPath; - private Path tempDownloadPath; + private final Path tempDownloadPath; private Path cwd; private Path downloadPath; - private Path toolRepositoryPath; + private final Path toolRepositoryPath; protected Path userHome; @@ -241,6 +243,7 @@ public void setCwd(Path userDir, String workspace, Path ideHome) { this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName); this.confPath = this.ideHome.resolve(FOLDER_CONF); this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS); + this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID); this.softwarePath = this.ideHome.resolve(FOLDER_SOFTWARE); this.softwareExtraPath = this.softwarePath.resolve(FOLDER_EXTRA); this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS); @@ -308,11 +311,10 @@ private boolean isIdeHome(Path dir) { private EnvironmentVariables createVariables() { AbstractEnvironmentVariables system = createSystemVariables(); - AbstractEnvironmentVariables user = extendVariables(system, this.userHomeIde, EnvironmentVariablesType.USER); - AbstractEnvironmentVariables settings = extendVariables(user, this.settingsPath, EnvironmentVariablesType.SETTINGS); - // TODO should we keep this workspace properties? Was this feature ever used? - AbstractEnvironmentVariables workspace = extendVariables(settings, this.workspacePath, EnvironmentVariablesType.WORKSPACE); - AbstractEnvironmentVariables conf = extendVariables(workspace, this.confPath, EnvironmentVariablesType.CONF); + AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER); + AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS); + AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE); + AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF); return conf.resolved(); } @@ -321,26 +323,6 @@ protected AbstractEnvironmentVariables createSystemVariables() { return EnvironmentVariables.ofSystem(this); } - protected AbstractEnvironmentVariables extendVariables(AbstractEnvironmentVariables envVariables, Path propertiesPath, EnvironmentVariablesType type) { - - Path propertiesFile = null; - if (propertiesPath == null) { - trace("Configuration directory for type {} does not exist.", type); - } else if (Files.isDirectory(propertiesPath)) { - propertiesFile = propertiesPath.resolve(EnvironmentVariables.DEFAULT_PROPERTIES); - boolean legacySupport = (type != EnvironmentVariablesType.USER); - if (legacySupport && !Files.exists(propertiesFile)) { - Path legacyFile = propertiesPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES); - if (Files.exists(legacyFile)) { - propertiesFile = legacyFile; - } - } - } else { - debug("Configuration directory {} does not exist.", propertiesPath); - } - return envVariables.extend(propertiesFile, type); - } - @Override public SystemInfo getSystemInfo() { @@ -430,6 +412,31 @@ public Path getSettingsPath() { return this.settingsPath; } + @Override + public Path getSettingsGitRepository() { + + Path settingsPath = getSettingsPath(); + + if (settingsPath == null) { + error("No settings repository was found."); + return null; + } + + // check whether the settings path has a .git folder only if its not a symbolic link + if (!Files.exists(settingsPath.resolve(".git")) && !Files.isSymbolicLink(settingsPath)) { + error("Settings repository exists but is not a git repository."); + return null; + } + + return settingsPath; + } + + @Override + public Path getSettingsCommitIdPath() { + + return this.settingsCommitIdPath; + } + @Override public Path getConfPath() { @@ -857,10 +864,11 @@ private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) { if (cmd.isIdeHomeRequired()) { debug(getMessageIdeHomeFound()); } - if (this.settingsPath != null) { - if (getGitContext().isRepositoryUpdateAvailable(this.settingsPath) || - (getGitContext().fetchIfNeeded(this.settingsPath) && getGitContext().isRepositoryUpdateAvailable(this.settingsPath))) { - interaction("Updates are available for the settings repository. If you want to pull the latest changes, call ide update."); + Path settingsRepository = getSettingsGitRepository(); + if (settingsRepository != null) { + if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || + (getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()))) { + interaction("Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\""); } } } 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 0273629a5..c1ee5dd3f 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 @@ -136,6 +136,13 @@ public interface IdeContext extends IdeStartContext { /** Legacy folder name used as compatibility fallback if {@link #FOLDER_TEMPLATES} does not exist. */ String FOLDER_LEGACY_TEMPLATES = "devon"; + /** The filename of the configuration file in the settings for this {@link CustomToolRepository}. */ + String FILE_CUSTOM_TOOLS = "ide-custom-tools.json"; + + /** + * file containing the current local commit hash of the settings repository. */ + String SETTINGS_COMMIT_ID = ".commit.id"; + /** * @return {@code true} if {@link #isOfflineMode() offline mode} is active or we are NOT {@link #isOnline() online}, {@code false} otherwise. */ @@ -351,6 +358,17 @@ default void requireOnline(String purpose) { */ Path getSettingsPath(); + /** + * + * @return the {@link Path} to the {@code settings} folder with the cloned git repository containing the project configuration only if the settings repository is in fact a git repository. + */ + Path getSettingsGitRepository(); + + /** + * @return the {@link Path} to the file containing the last tracked commit Id of the settings repository. + */ + Path getSettingsCommitIdPath(); + /** * @return the {@link Path} to the templates folder inside the {@link #getSettingsPath() settings}. The relative directory structure in this templates folder * is to be applied to {@link #getIdeHome() IDE_HOME} when the project is set up. diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java index 52445bf7b..2caeafab2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java @@ -95,9 +95,7 @@ public VariableSource getSource() { protected boolean isExported(String name) { if (this.parent != null) { - if (this.parent.isExported(name)) { - return true; - } + return this.parent.isExported(name); } return false; } @@ -144,13 +142,14 @@ protected VariableLine createVariableLine(String name, boolean onlyExported, Abs } /** - * @param propertiesFilePath the {@link #getPropertiesFilePath() propertiesFilePath} of the child {@link EnvironmentVariables}. + * @param propertiesFolderPath the {@link Path} to the folder containing the {@link #getPropertiesFilePath() properties file} of the child + * {@link EnvironmentVariables}. * @param type the {@link #getType() type}. * @return the new {@link EnvironmentVariables}. */ - public AbstractEnvironmentVariables extend(Path propertiesFilePath, EnvironmentVariablesType type) { + public AbstractEnvironmentVariables extend(Path propertiesFolderPath, EnvironmentVariablesType type) { - return new EnvironmentVariablesPropertiesFile(this, type, propertiesFilePath, this.context); + return new EnvironmentVariablesPropertiesFile(this, type, propertiesFolderPath, null, this.context); } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java index de52a53e8..dbb45f538 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java @@ -137,6 +137,16 @@ default EnvironmentVariables getParent() { return null; } + /** + * @param name the {@link com.devonfw.tools.ide.variable.VariableDefinition#getName() name} of the variable to set. + * @param value the new {@link #get(String) value} of the variable to set. May be {@code null} to unset the variable. + * @return the old variable value. + */ + default String set(String name, String value) { + + throw new UnsupportedOperationException(); + } + /** * @param name the {@link com.devonfw.tools.ide.variable.VariableDefinition#getName() name} of the variable to set. * @param value the new {@link #get(String) value} of the variable to set. May be {@code null} to unset the variable. @@ -263,5 +273,4 @@ static String getToolEditionVariable(String tool) { return tool.toUpperCase(Locale.ROOT) + "_EDITION"; } - } diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java index 877b43295..cc78465e2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -27,7 +26,9 @@ public final class EnvironmentVariablesPropertiesFile extends EnvironmentVariabl private final EnvironmentVariablesType type; - private Path propertiesFilePath; + private final Path propertiesFilePath; + + private final Path legacyPropertiesFilePath; private final Map variables; @@ -35,6 +36,8 @@ public final class EnvironmentVariablesPropertiesFile extends EnvironmentVariabl private final Set modifiedVariables; + private Boolean legacyConfiguration; + /** * The constructor. * @@ -46,29 +49,80 @@ public final class EnvironmentVariablesPropertiesFile extends EnvironmentVariabl public EnvironmentVariablesPropertiesFile(AbstractEnvironmentVariables parent, EnvironmentVariablesType type, Path propertiesFilePath, IdeContext context) { + this(parent, type, getParent(propertiesFilePath), propertiesFilePath, context); + } + + /** + * The constructor. + * + * @param parent the parent {@link EnvironmentVariables} to inherit from. + * @param type the {@link #getType() type}. + * @param propertiesFolderPath the {@link Path} to the folder where the properties file is expected. + * @param propertiesFilePath the {@link #getSource() source}. + * @param context the {@link IdeContext}. + */ + public EnvironmentVariablesPropertiesFile(AbstractEnvironmentVariables parent, EnvironmentVariablesType type, Path propertiesFolderPath, + Path propertiesFilePath, IdeContext context) { + super(parent, context); Objects.requireNonNull(type); assert (type != EnvironmentVariablesType.RESOLVED); this.type = type; - this.propertiesFilePath = propertiesFilePath; + if (propertiesFolderPath == null) { + this.propertiesFilePath = null; + this.legacyPropertiesFilePath = null; + } else { + if (propertiesFilePath == null) { + this.propertiesFilePath = propertiesFolderPath.resolve(DEFAULT_PROPERTIES); + } else { + this.propertiesFilePath = propertiesFilePath; + assert (propertiesFilePath.getParent().equals(propertiesFolderPath)); + } + Path legacyPropertiesFolderPath = propertiesFolderPath; + if (type == EnvironmentVariablesType.USER) { + // ~/devon.properties vs. ~/.ide/ide.properties + legacyPropertiesFolderPath = propertiesFolderPath.getParent(); + } + this.legacyPropertiesFilePath = legacyPropertiesFolderPath.resolve(LEGACY_PROPERTIES); + } this.variables = new HashMap<>(); this.exportedVariables = new HashSet<>(); this.modifiedVariables = new HashSet<>(); load(); } + private static Path getParent(Path path) { + + if (path == null) { + return null; + } + return path.getParent(); + } + private void load() { - if (this.propertiesFilePath == null) { - return; + boolean success = load(this.propertiesFilePath); + if (success) { + this.legacyConfiguration = Boolean.FALSE; + } else { + success = load(this.legacyPropertiesFilePath); + if (success) { + this.legacyConfiguration = Boolean.TRUE; + } } - if (!Files.exists(this.propertiesFilePath)) { - this.context.trace("Properties not found at {}", this.propertiesFilePath); - return; + } + + private boolean load(Path file) { + if (file == null) { + return false; } - this.context.trace("Loading properties from {}", this.propertiesFilePath); - boolean legacyProperties = this.propertiesFilePath.getFileName().toString().equals(LEGACY_PROPERTIES); - try (BufferedReader reader = Files.newBufferedReader(this.propertiesFilePath)) { + if (!Files.exists(file)) { + this.context.trace("Properties not found at {}", file); + return false; + } + this.context.trace("Loading properties from {}", file); + boolean legacyProperties = file.getFileName().toString().equals(LEGACY_PROPERTIES); + try (BufferedReader reader = Files.newBufferedReader(file)) { String line; do { line = reader.readLine(); @@ -86,7 +140,7 @@ private void load() { boolean legacyVariable = IdeVariables.isLegacyVariable(name); if (legacyVariable && !legacyProperties) { this.context.warning("Legacy variable name is used to define variable {} in {} - please cleanup your configuration.", variableLine, - this.propertiesFilePath); + file); } String oldValue = this.variables.get(migratedName); if (oldValue != null) { @@ -94,10 +148,10 @@ private void load() { if (legacyVariable) { // if the legacy name was configured we do not want to override the official variable! this.context.warning("Both legacy variable {} and official variable {} are configured in {} - ignoring legacy variable declaration!", - variableDefinition.getLegacyName(), variableDefinition.getName(), this.propertiesFilePath); + variableDefinition.getLegacyName(), variableDefinition.getName(), file); } else { this.context.warning("Duplicate variable definition {} with old value '{}' and new value '{}' in {}", name, oldValue, migratedValue, - this.propertiesFilePath); + file); this.variables.put(migratedName, migratedValue); } } else { @@ -109,58 +163,39 @@ private void load() { } } } while (line != null); + return true; } catch (IOException e) { - throw new IllegalStateException("Failed to load properties from " + this.propertiesFilePath, e); + throw new IllegalStateException("Failed to load properties from " + file, e); } } @Override public void save() { - if (this.modifiedVariables.isEmpty()) { + boolean isLegacy = Boolean.TRUE.equals(this.legacyConfiguration); + if (this.modifiedVariables.isEmpty() && !isLegacy) { this.context.trace("No changes to save in properties file {}", this.propertiesFilePath); return; } - Path newPropertiesFilePath = this.propertiesFilePath; - String propertiesFileName = this.propertiesFilePath.getFileName().toString(); - Path propertiesParentPath = newPropertiesFilePath.getParent(); - if (LEGACY_PROPERTIES.equals(propertiesFileName)) { - newPropertiesFilePath = propertiesParentPath.resolve(DEFAULT_PROPERTIES); - this.context.info("Converting legacy properties to {}", newPropertiesFilePath); + Path file = this.propertiesFilePath; + if (isLegacy) { + this.context.info("Converting legacy properties to {}", this.propertiesFilePath); + file = this.legacyPropertiesFilePath; } - this.context.getFileAccess().mkdirs(propertiesParentPath); - List lines = new ArrayList<>(); + List lines = loadVariableLines(file); - // Skip reading if the file does not exist - if (Files.exists(this.propertiesFilePath)) { - try (BufferedReader reader = Files.newBufferedReader(this.propertiesFilePath)) { - String line; - do { - line = reader.readLine(); - if (line != null) { - VariableLine variableLine = VariableLine.of(line, this.context, getSource()); - lines.add(variableLine); - } - } while (line != null); - } catch (IOException e) { - throw new IllegalStateException("Failed to load existing properties from " + this.propertiesFilePath, e); - } - } else { - this.context.debug("Properties file {} does not exist, skipping read.", newPropertiesFilePath); - } - - try (BufferedWriter writer = Files.newBufferedWriter(newPropertiesFilePath, StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING)) { + this.context.getFileAccess().mkdirs(this.propertiesFilePath.getParent()); + try (BufferedWriter writer = Files.newBufferedWriter(this.propertiesFilePath)) { // copy and modify original lines from properties file for (VariableLine line : lines) { VariableLine newLine = migrateLine(line, true); if (newLine == null) { - this.context.debug("Removed variable line '{}' from {}", line, newPropertiesFilePath); + this.context.debug("Removed variable line '{}' from {}", line, this.propertiesFilePath); } else { if (newLine != line) { - this.context.debug("Changed variable line from '{}' to '{}' in {}", line, newLine, newPropertiesFilePath); + this.context.debug("Changed variable line from '{}' to '{}' in {}", line, newLine, this.propertiesFilePath); } writer.append(newLine.toString()); writer.append(NEWLINE); @@ -184,9 +219,31 @@ public void save() { } this.modifiedVariables.clear(); } catch (IOException e) { - throw new IllegalStateException("Failed to save properties to " + newPropertiesFilePath, e); + throw new IllegalStateException("Failed to save properties to " + this.propertiesFilePath, e); + } + this.legacyConfiguration = Boolean.FALSE; + } + + private List loadVariableLines(Path file) { + List lines = new ArrayList<>(); + if (!Files.exists(file)) { + // Skip reading if the file does not exist + this.context.debug("Properties file {} does not exist, skipping read.", file); + return lines; + } + try (BufferedReader reader = Files.newBufferedReader(file)) { + String line; + do { + line = reader.readLine(); + if (line != null) { + VariableLine variableLine = VariableLine.of(line, this.context, getSource()); + lines.add(variableLine); + } + } while (line != null); + } catch (IOException e) { + throw new IllegalStateException("Failed to load existing properties from " + file, e); } - this.propertiesFilePath = newPropertiesFilePath; + return lines; } private VariableLine migrateLine(VariableLine line, boolean saveNotLoad) { @@ -232,7 +289,7 @@ protected void collectVariables(Map variables, boolean onl } @Override - protected boolean isExported(String name) { + public boolean isExported(String name) { if (this.exportedVariables.contains(name)) { return true; @@ -255,17 +312,22 @@ public Path getPropertiesFilePath() { @Override public Path getLegacyPropertiesFilePath() { - if (this.propertiesFilePath == null) { - return null; - } - if (this.propertiesFilePath.getFileName().toString().equals(LEGACY_PROPERTIES)) { - return this.propertiesFilePath; - } - Path legacyProperties = this.propertiesFilePath.getParent().resolve(LEGACY_PROPERTIES); - if (Files.exists(legacyProperties)) { - return legacyProperties; - } - return null; + return this.legacyPropertiesFilePath; + } + + /** + * @return {@code Boolean#TRUE} if the current variable state comes from {@link #getLegacyPropertiesFilePath()}, {@code Boolean#FALSE} if state comes from + * {@link #getPropertiesFilePath()}), and {@code null} if neither of these files existed (nothing was loaded). + */ + public Boolean getLegacyConfiguration() { + + return this.legacyConfiguration; + } + + @Override + public String set(String name, String value) { + + return set(name, value, this.exportedVariables.contains(name)); } @Override @@ -287,4 +349,18 @@ public String set(String name, String value, boolean export) { return oldValue; } + /** + * Removes a property. + * + * @param name name of the property to remove. + */ + public void remove(String name) { + String oldValue = this.variables.remove(name); + if (oldValue != null) { + this.modifiedVariables.add(name); + this.exportedVariables.remove(name); + this.context.debug("Removed variable name of '{}' in {}", name, this.propertiesFilePath); + } + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/VariableLine.java b/cli/src/main/java/com/devonfw/tools/ide/environment/VariableLine.java index 7d89de937..0314a3efa 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/VariableLine.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/VariableLine.java @@ -1,5 +1,8 @@ package com.devonfw.tools.ide.environment; +import java.util.ArrayList; +import java.util.List; + import com.devonfw.tools.ide.log.IdeLogger; /** @@ -314,4 +317,26 @@ public static VariableLine of(boolean export, String name, String value, Variabl return new Variable(export, name, value, null, source); } + /** + * Returns a list of String Variables. + * + * @param value String to parse + * @return List of variables. + */ + public static List parseArray(String value) { + String csv = value; + String separator = ","; + // TODO: refactor with isBashArray method from VariableDefinitionStringList + if (value.startsWith("(") && value.endsWith(")")) { + csv = value.substring(1, value.length() - 1); + separator = " "; + } + String[] items = csv.split(separator); + List list = new ArrayList<>(items.length); + for (String item : items) { + list.add(item.trim()); + } + return list; + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java index fe5af87e9..6a27c4de4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java @@ -58,13 +58,23 @@ public interface GitContext { * Checks if there are updates available for the Git repository in the specified target folder by comparing the local commit hash with the remote commit * hash. * - * @param repository the {@link Path} to the target folder where the git repository is located. This should be the folder containing the ".git" - * subfolder. + * @param repository the {@link Path} to the target folder where the git repository is located. * @return {@code true} if the remote repository contains commits that are not present in the local repository, indicating that updates are available. * {@code false} if the local and remote repositories are in sync, or if there was an issue retrieving the commit hashes. */ boolean isRepositoryUpdateAvailable(Path repository); + /** + * Checks if there are updates available for the Git repository in the specified target folder by comparing the local commit hash with the remote commit + * hash. + * + * @param repository the {@link Path} to the target folder where the git repository is located. + * @param trackedCommitIdPath the {@link Path} to a file containing the last tracked commit ID of this repository. + * @return {@code true} if the remote repository contains commits that are not present in the local repository, indicating that updates are available. + * {@code false} if the local and remote repositories are in sync, or if there was an issue retrieving the commit hashes. + */ + boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath); + /** * Attempts a git pull and reset if required. * @@ -175,4 +185,11 @@ default void reset(Path repository, String branch) { */ String determineRemote(Path repository); + /** + * Saves the current git commit ID of a repository to a file given as an argument. + * + * @param repository the path to the git repository + * @param trackedCommitIdPath the path to the file where the commit Id will be written. + */ + void saveCurrentCommitId(Path repository, Path trackedCommitIdPath); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java index eda59a8e4..185f9e2a3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.git; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -59,6 +60,21 @@ public boolean isRepositoryUpdateAvailable(Path repository) { return !localCommitId.equals(remoteCommitId); } + @Override + public boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath) { + + verifyGitInstalled(); + String trackedCommitId; + try { + trackedCommitId = Files.readString(trackedCommitIdPath); + } catch (IOException e) { + return false; + } + + String remoteCommitId = runGitCommandAndGetSingleOutput("Failed to get the remote commit id.", repository, "rev-parse", "@{u}"); + return !trackedCommitId.equals(remoteCommitId); + } + @Override public void pullOrCloneAndResetIfNeeded(GitUrl gitUrl, Path repository, String remoteName) { @@ -292,6 +308,24 @@ private ProcessResult runGitCommand(Path directory, ProcessMode mode, ProcessErr processContext.addArgs(args); return processContext.run(mode); } + + @Override + public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) { + + if ((repository == null) || (trackedCommitIdPath == null)) { + this.context.warning("Invalid usage of saveCurrentCommitId with null value"); + return; + } + this.context.trace("Saving commit Id of {} into {}", repository, trackedCommitIdPath); + String currentCommitId = runGitCommandAndGetSingleOutput("Failed to get current commit id.", repository, "rev-parse", "HEAD"); + if (currentCommitId != null) { + try { + Files.writeString(trackedCommitIdPath, currentCommitId); + } catch (IOException e) { + throw new IllegalStateException("Failed to save commit ID", e); + } + } + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java index 2245f155d..c77d7f281 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java @@ -43,6 +43,21 @@ public String toString() { return this.url + "#" + this.branch; } + /** + * Extracts the project name from an git URL. + * For URLs like "https://github.com/devonfw/ide-urls.git" returns "ide-urls" + * + * @return the project name without ".git" extension + */ + public String getProjectName() { + String path = this.url.substring(this.url.indexOf("://") + 3); + if (path.endsWith(".git")) { + path = path.substring(0, path.length() - 4); + } + String[] parts = path.split("/"); + return parts[parts.length - 1]; + } + /** * @param gitUrl the {@link #toString() string representation} of a {@link GitUrl}. May contain a branch name as {@code «url»#«branch»}. * @return the parsed {@link GitUrl}. diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java index a9636c2e3..7b03134e6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; /** @@ -209,6 +210,9 @@ default void extract(Path archiveFile, Path targetDir, Consumer postExtrac /** * Deletes the given {@link Path} idempotent and recursive. + *

+ * ATTENTION: In most cases we want to use {@link #backup(Path)} instead to prevent the user from data loss. + *

* * @param path the {@link Path} to delete. */ @@ -237,7 +241,19 @@ default void extract(Path archiveFile, Path targetDir, Consumer postExtrac * @return all children of the given {@link Path} that match the given {@link Predicate}. Will be the empty list of the given {@link Path} is not an existing * directory. */ - List listChildren(Path dir, Predicate filter); + default List listChildren(Path dir, Predicate filter) { + + return listChildrenMapped(dir, child -> (filter.test(child)) ? child : null); + } + + /** + * @param dir the {@link Path} to the directory where to list the children. + * @param filter the filter {@link Function} used to {@link Function#apply(Object) filter and transform} children to include. If the {@link Function} + * returns {@code null}, the child will be filtered, otherwise the returned {@link Path} will be included in the resulting {@link List}. + * @return all children of the given {@link Path} returned by the given {@link Function}. Will be the empty list if the given {@link Path} is not an existing + * directory. + */ + List listChildrenMapped(Path dir, Function filter); /** * Finds the existing file with the specified name in the given list of directories. diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java index ae2f0b605..2100ad327 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java @@ -264,22 +264,32 @@ private boolean isJunction(Path path) { @Override public void backup(Path fileOrFolder) { - if (Files.isSymbolicLink(fileOrFolder) || isJunction(fileOrFolder)) { + if ((fileOrFolder != null) && (Files.isSymbolicLink(fileOrFolder) || isJunction(fileOrFolder))) { delete(fileOrFolder); - } else { - // fileOrFolder is a directory - Path backupPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_BACKUPS); + } else if ((fileOrFolder != null) && Files.exists(fileOrFolder)) { LocalDateTime now = LocalDateTime.now(); - String date = DateTimeUtil.formatDate(now); + String date = DateTimeUtil.formatDate(now, true); String time = DateTimeUtil.formatTime(now); - Path backupDatePath = backupPath.resolve(date); - mkdirs(backupDatePath); - Path target = backupDatePath.resolve(fileOrFolder.getFileName().toString() + "_" + time); + String filename = fileOrFolder.getFileName().toString(); + Path backupPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_BACKUPS).resolve(date).resolve(time + "_" + filename); + backupPath = appendParentPath(backupPath, fileOrFolder.getParent(), 2); + mkdirs(backupPath); + Path target = backupPath.resolve(filename); this.context.info("Creating backup by moving {} to {}", fileOrFolder, target); move(fileOrFolder, target); + } else { + this.context.trace("Backup of {} skipped as the path does not exist.", fileOrFolder); } } + private static Path appendParentPath(Path path, Path parent, int max) { + + if ((parent == null) || (max <= 0)) { + return path; + } + return appendParentPath(path, parent.getParent(), max - 1).resolve(parent.getFileName()); + } + @Override public void move(Path source, Path targetDir) { @@ -829,7 +839,7 @@ private Path findFirstRecursive(Path dir, Predicate filter, boolean recurs } @Override - public List listChildren(Path dir, Predicate filter) { + public List listChildrenMapped(Path dir, Function filter) { if (!Files.isDirectory(dir)) { return List.of(); @@ -839,9 +849,14 @@ public List listChildren(Path dir, Predicate filter) { Iterator iterator = childStream.iterator(); while (iterator.hasNext()) { Path child = iterator.next(); - if (filter.test(child)) { - this.context.trace("Accepted file {}", child); - children.add(child); + Path filteredChild = filter.apply(child); + if (filteredChild != null) { + if (filteredChild == child) { + this.context.trace("Accepted file {}", child); + } else { + this.context.trace("Accepted file {} and mapped to {}", child, filteredChild); + } + children.add(filteredChild); } else { this.context.trace("Ignoring file {} according to filter", child); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/AbstractWorkspaceMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/AbstractWorkspaceMerger.java index 366cbbcdd..0d4378754 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/AbstractWorkspaceMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/AbstractWorkspaceMerger.java @@ -8,8 +8,6 @@ /** * {@link WorkspaceMerger} responsible for a single type of {@link Path}. - * - * @since 3.0.0 */ public abstract class AbstractWorkspaceMerger implements WorkspaceMerger { diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/DirectoryMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/DirectoryMerger.java index 27564e8c0..9308ab64c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/DirectoryMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/DirectoryMerger.java @@ -8,6 +8,7 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import org.jline.utils.Log; @@ -51,6 +52,7 @@ public DirectoryMerger(IdeContext context) { TextMerger textMerger = new TextMerger(context); this.extension2mergerMap.put("name", textMerger); // intellij specific this.extension2mergerMap.put("editorconfig", textMerger); + this.extension2mergerMap.put("txt", textMerger); this.fallbackMerger = new FallbackMerger(context); } @@ -99,8 +101,8 @@ public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean return; } Log.trace("Traversing directory: {}", update); - try { - Iterator iterator = Files.list(update).iterator(); + try (Stream childStream = Files.list(update)) { + Iterator iterator = childStream.iterator(); while (iterator.hasNext()) { Path updateChild = iterator.next(); Path fileName = updateChild.getFileName(); @@ -125,8 +127,8 @@ private Set addChildren(Path folder, Set children) { if (!Files.isDirectory(folder)) { return children; } - try { - Iterator iterator = Files.list(folder).iterator(); + try (Stream childStream = Files.list(folder)) { + Iterator iterator = childStream.iterator(); while (iterator.hasNext()) { Path child = iterator.next(); if (children == null) { @@ -140,4 +142,23 @@ private Set addChildren(Path folder, Set children) { } } + @Override + public void upgrade(Path folder) { + + try (Stream childStream = Files.list(folder)) { + Iterator iterator = childStream.iterator(); + while (iterator.hasNext()) { + Path child = iterator.next(); + if (Files.isDirectory(child)) { + upgrade(child); + } else { + FileMerger merger = getMerger(child); + merger.upgrade(child); + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to list children of folder " + folder, e); + } + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/FallbackMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/FallbackMerger.java index 54bd3bae2..67a78a0cc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/FallbackMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/FallbackMerger.java @@ -8,8 +8,6 @@ /** * Implementation of {@link FileMerger} to use as fallback. It can not actually merge but will simply overwrite the files. - * - * @since 3.0.0 */ public class FallbackMerger extends FileMerger { @@ -39,4 +37,9 @@ public void inverseMerge(Path workspaceFile, EnvironmentVariables resolver, bool // nothing by default, we could copy the workspace file back to the update file if it exists... } + @Override + protected boolean doUpgrade(Path workspaceFile) throws Exception { + + return false; + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/FileMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/FileMerger.java index c931f17df..a1333585a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/FileMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/FileMerger.java @@ -1,12 +1,16 @@ package com.devonfw.tools.ide.merge; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.regex.Matcher; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.variable.IdeVariables; +import com.devonfw.tools.ide.variable.VariableDefinition; +import com.devonfw.tools.ide.variable.VariableSyntax; /** * {@link WorkspaceMerger} responsible for a single type of file. @@ -23,7 +27,7 @@ public abstract class FileMerger extends AbstractWorkspaceMerger { public FileMerger(IdeContext context) { super(context); - this.legacySupport = IdeVariables.IDE_VARIABLE_SYNTAX_LEGACY_SUPPORT_ENABLED.get(context).booleanValue(); + this.legacySupport = Boolean.TRUE.equals(IdeVariables.IDE_VARIABLE_SYNTAX_LEGACY_SUPPORT_ENABLED.get(context)); } /** @@ -60,4 +64,80 @@ public final int merge(Path setup, Path update, EnvironmentVariables variables, * @param workspace the workspace {@link Path} to create or update. */ protected abstract void doMerge(Path setup, Path update, EnvironmentVariables variables, Path workspace); + + @Override + public void upgrade(Path workspaceFile) { + + try { + boolean modified = doUpgrade(workspaceFile); + if (modified) { + this.context.debug("Successfully migrated file {}", workspaceFile); + } else { + this.context.trace("Nothing to migrate in file {}", workspaceFile); + } + } catch (Exception e) { + throw new IllegalStateException("Failed to update file " + workspaceFile, e); + } + } + + /** + * @param workspaceFile the {@link Path} to the file to migrate. + * @return {@code true} if the file was migrated (modified), {@code false} otherwise. + * @throws Exception on error. + * @see #upgrade(Path) + */ + protected abstract boolean doUpgrade(Path workspaceFile) throws Exception; + + /** + * Implementation for {@link #doUpgrade(Path)} in case of simple text file format. + * + * @param workspaceFile the {@link Path} to the file to migrate. + * @return {@code true} if the file was migrated (modified), {@code false} otherwise. + * @throws IOException on error. + */ + protected boolean doUpgradeTextContent(Path workspaceFile) throws IOException { + + String content = Files.readString(workspaceFile); + String migratedContent = upgradeWorkspaceContent(content); + boolean modified = !migratedContent.equals(content); + if (modified) { + Files.writeString(workspaceFile, migratedContent); + } + return modified; + } + + /** + * @param content the content from a workspace template file that may contain legacy stuff like old variable syntax. + * @return the given {@link String} with potential legacy constructs being resolved. + */ + protected String upgradeWorkspaceContent(String content) { + + VariableSyntax syntax = VariableSyntax.CURLY; + Matcher matcher = syntax.getPattern().matcher(content); + StringBuilder sb = null; + while (matcher.find()) { + if (sb == null) { + sb = new StringBuilder(content.length() + 8); + } + String variableName = syntax.getVariable(matcher); + String replacement; + VariableDefinition variableDefinition = IdeVariables.get(variableName); + if (variableDefinition != null) { + variableName = variableDefinition.getName(); // ensure legacy name gets replaced with new name + replacement = VariableSyntax.SQUARE.create(variableName); + } else if (variableName.equals("SETTINGS_PATH")) { + replacement = "$[IDE_HOME]/settings"; + } else { + replacement = matcher.group(); // leave ${variableName} untouched + } + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } + if (sb == null) { + return content; + } + matcher.appendTail(sb); + return sb.toString().replace("\\conf\\.m2\\", "/conf/.m2/").replace("/conf/.m2/", "/conf/mvn/"); + } + + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java index 8ea8f5c21..9b9e88392 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java @@ -1,11 +1,14 @@ package com.devonfw.tools.ide.merge; +import java.io.IOException; import java.io.OutputStream; import java.io.Reader; +import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Set; import jakarta.json.Json; @@ -23,6 +26,13 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; /** * Implementation of {@link FileMerger} for JSON. @@ -225,6 +235,92 @@ private JsonValue mergeAndResolveNativeType(JsonValue json, JsonValue mergeJson, } } + @Override + protected boolean doUpgrade(Path workspaceFile) throws Exception { + + JsonNode jsonNode; + ObjectMapper mapper = new ObjectMapper(); + try (Reader reader = Files.newBufferedReader(workspaceFile)) { + jsonNode = mapper.reader().readTree(reader); + } + JsonNode migratedNode = upgradeJsonNode(jsonNode); + boolean modified = (migratedNode != jsonNode); + if (migratedNode == null) { + migratedNode = jsonNode; + } + if (modified) { + try (Writer writer = Files.newBufferedWriter(workspaceFile)) { + mapper.writer(new JsonPrettyPrinter()).writeValue(writer, migratedNode); + } + } + return modified; + } + + /** + * @param jsonNode the {@link JsonNode} to upgrade. + * @return the given {@link JsonNode} if unmodified after upgrade. Otherwise, a new migrated {@link JsonNode} or {@code null} if the given {@link JsonNode} + * was mutable and the migration could be applied directly. + */ + private JsonNode upgradeJsonNode(JsonNode jsonNode) { + + if (jsonNode instanceof ArrayNode jsonArray) { + return upgradeJsonArray(jsonArray); + } else if (jsonNode instanceof ObjectNode jsonObject) { + return upgradeJsonObject(jsonObject); + } else if (jsonNode instanceof TextNode jsonString) { + return upgradeJsonString(jsonString); + } else { + assert jsonNode.isValueNode(); + return jsonNode; + } + } + + private ObjectNode upgradeJsonObject(ObjectNode jsonObject) { + + ObjectNode result = jsonObject; + Iterator fieldNames = jsonObject.fieldNames(); + while (fieldNames.hasNext()) { + String fieldName = fieldNames.next(); + JsonNode child = jsonObject.get(fieldName); + JsonNode migratedChild = upgradeJsonNode(child); + if (migratedChild != child) { + result = null; + if (migratedChild != null) { + jsonObject.put(fieldName, migratedChild); + } + } + } + return result; + } + + private ArrayNode upgradeJsonArray(ArrayNode jsonArray) { + + ArrayNode result = jsonArray; + int size = jsonArray.size(); + for (int i = 0; i < size; i++) { + JsonNode child = jsonArray.get(i); + JsonNode migratedChild = upgradeJsonNode(child); + if (migratedChild != child) { + result = null; + if (migratedChild != null) { + jsonArray.set(i, migratedChild); + } + } + } + return result; + } + + private JsonNode upgradeJsonString(TextNode jsonString) { + + String text = jsonString.textValue(); + String migratedText = upgradeWorkspaceContent(text); + if (migratedText.equals(text)) { + return jsonString; + } else { + return new TextNode(migratedText); + } + } + private static class Status { /** @@ -264,4 +360,37 @@ private Status(boolean inverse, boolean addNewProperties) { } + /** + * Extends {@link DefaultPrettyPrinter} to get nicely formatted JSON output. + */ + private static class JsonPrettyPrinter extends DefaultPrettyPrinter { + + public JsonPrettyPrinter() { + DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter(" ", "\n"); + indentObjectsWith(indenter); + indentArraysWith(indenter); + _objectFieldValueSeparatorWithSpaces = ": "; + } + + private JsonPrettyPrinter(JsonPrettyPrinter pp) { + super(pp); + } + + @Override + public void writeEndArray(com.fasterxml.jackson.core.JsonGenerator g, int nrOfValues) throws IOException { + + if (!_arrayIndenter.isInline()) { + _nesting--; + } + if (nrOfValues > 0) { + _arrayIndenter.writeIndentation(g, _nesting); + } + g.writeRaw(']'); + } + + @Override + public DefaultPrettyPrinter createInstance() { + return new JsonPrettyPrinter(this); + } + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/PropertiesMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/PropertiesMerger.java index 8fddfc6d4..00a110011 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/PropertiesMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/PropertiesMerger.java @@ -8,8 +8,6 @@ import java.util.Properties; import java.util.Set; -import org.jline.utils.Log; - import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.SortedProperties; @@ -37,7 +35,7 @@ protected void doMerge(Path setup, Path update, EnvironmentVariables resolver, P Path template = setup; if (Files.exists(workspace)) { if (!updateFileExists) { - Log.trace("Nothing to do as update file does not exist: {}", update); + this.context.trace("Nothing to do as update file does not exist: {}", update); return; // nothing to do ... } load(properties, workspace); @@ -50,14 +48,14 @@ protected void doMerge(Path setup, Path update, EnvironmentVariables resolver, P } resolve(properties, resolver, template.toString()); save(properties, workspace); - Log.trace("Saved merged properties to: {}", workspace); + this.context.trace("Saved merged properties to: {}", workspace); } /** * @param file the {@link Path} to load. * @return the loaded {@link Properties}. */ - public static Properties load(Path file) { + public Properties load(Path file) { Properties properties = new Properties(); load(properties, file); @@ -68,14 +66,14 @@ public static Properties load(Path file) { * @param file the {@link Path} to load. * @return the loaded {@link Properties}. */ - public static Properties loadIfExists(Path file) { + public Properties loadIfExists(Path file) { Properties properties = new Properties(); if (file != null) { if (Files.exists(file)) { load(properties, file); } else { - Log.trace("Properties file does not exist: {}", file); + this.context.trace("Properties file does not exist: {}", file); } } return properties; @@ -85,9 +83,9 @@ public static Properties loadIfExists(Path file) { * @param properties the existing {@link Properties} instance. * @param file the properties {@link Path} to load. */ - public static void load(Properties properties, Path file) { + public void load(Properties properties, Path file) { - Log.trace("Loading properties file: {}", file); + this.context.trace("Loading properties file: {}", file); try (Reader reader = Files.newBufferedReader(file)) { properties.load(reader); } catch (IOException e) { @@ -108,9 +106,9 @@ private void resolve(Properties properties, EnvironmentVariables variables, Obje * @param properties the {@link Properties} to save. * @param file the {@link Path} to save to. */ - public static void save(Properties properties, Path file) { + public void save(Properties properties, Path file) { - Log.trace("Saving properties file: {}", file); + this.context.trace("Saving properties file: {}", file); ensureParentDirectoryExists(file); try (Writer writer = Files.newBufferedWriter(file)) { properties.store(writer, null); @@ -123,11 +121,11 @@ public static void save(Properties properties, Path file) { public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update) { if (!Files.exists(workspace)) { - Log.trace("Workspace file does not exist: {}", workspace); + this.context.trace("Workspace file does not exist: {}", workspace); return; } if (!Files.exists(update)) { - Log.trace("Update file does not exist: {}", update); + this.context.trace("Update file does not exist: {}", update); return; } Object src = workspace.getFileName(); @@ -153,10 +151,15 @@ public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean } if (updated) { save(mergedProperties, update); - Log.debug("Saved changes from: {} to: {}", workspace.getFileName(), update); + this.context.debug("Saved changes from: {} to: {}", workspace.getFileName(), update); } else { - Log.trace("No changes for: {}", update); + this.context.trace("No changes for: {}", update); } } + @Override + protected boolean doUpgrade(Path workspaceFile) throws Exception { + + return doUpgradeTextContent(workspaceFile); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/TextMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/TextMerger.java index e1f71ea73..da4f2b623 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/TextMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/TextMerger.java @@ -59,4 +59,10 @@ protected void doMerge(Path setup, Path update, EnvironmentVariables variables, public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update) { } + + @Override + protected boolean doUpgrade(Path workspaceFile) throws Exception { + + return doUpgradeTextContent(workspaceFile); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/WorkspaceMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/WorkspaceMerger.java index e09ee7795..dfee5ed1d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/WorkspaceMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/WorkspaceMerger.java @@ -21,9 +21,16 @@ public interface WorkspaceMerger { /** * @param workspace the workspace {@link Path} where to get the changes from. * @param variables the {@link EnvironmentVariables} to {@link EnvironmentVariables#inverseResolve(String, Object) inverse resolve variables}. - * @param addNewProperties - {@code true} to also add new properties to the {@code updateFile}, {@code false} otherwise (to only update existing properties). + * @param addNewProperties - {@code true} to also add new properties to the {@code updateFile}, {@code false} otherwise (to only update existing + * properties). * @param update the update {@link Path} */ void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update); + /** + * @param workspace the {@link Path} to the {@link com.devonfw.tools.ide.context.IdeContext#FOLDER_WORKSPACE workspace} with IDE templates to upgrade + * (migrate and replace legacy constructs). + */ + void upgrade(Path workspace); + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/xmlmerger/XmlMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/xmlmerger/XmlMerger.java index 03d4931cb..48579230a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/xmlmerger/XmlMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/xmlmerger/XmlMerger.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.merge.xmlmerger; +import java.io.BufferedWriter; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -35,6 +36,7 @@ public class XmlMerger extends FileMerger { private static final TransformerFactory TRANSFORMER_FACTORY; + /** The namespace URI for this XML merger. */ public static final String MERGE_NS_URI = "https://github.com/devonfw/IDEasy/merge"; static { @@ -255,4 +257,81 @@ private void resolve(NamedNodeMap attributes, EnvironmentVariables variables, bo attribute.setValue(resolvedValue); } } + + @Override + protected boolean doUpgrade(Path workspaceFile) throws Exception { + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(workspaceFile.toFile()); + checkForXmlNamespace(document, workspaceFile); + boolean modified = updateWorkspaceXml(document.getDocumentElement()); + if (modified) { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + DOMSource source = new DOMSource(document); + try (BufferedWriter writer = Files.newBufferedWriter(workspaceFile)) { + StreamResult result = new StreamResult(writer); + transformer.transform(source, result); + } + } + return modified; + } + + private boolean updateWorkspaceXml(Element element) { + + boolean modified = false; + NamedNodeMap attributes = element.getAttributes(); + if (attributes != null) { + for (int i = 0; i < attributes.getLength(); i++) { + Node node = attributes.item(i); + if (node instanceof Attr attribute) { + String value = attribute.getValue(); + String migratedValue = upgradeWorkspaceContent(value); + if (!migratedValue.equals(value)) { + modified = true; + attribute.setValue(migratedValue); + } + } + } + } + + NodeList childNodes = element.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node childNode = childNodes.item(i); + boolean childModified = false; + if (childNode instanceof Element childElement) { + childModified = updateWorkspaceXml(childElement); + } else if (childNode instanceof Text childText) { + String text = childText.getTextContent(); + String migratedText = upgradeWorkspaceContent(text); + childModified = !migratedText.equals(text); + if (childModified) { + childText.setTextContent(migratedText); + } + } + if (childModified) { + modified = true; + } + } + return modified; + } + + private void checkForXmlNamespace(Document document, Path workspaceFile) { + + NamedNodeMap attributes = document.getDocumentElement().getAttributes(); + if (attributes != null) { + for (int i = 0; i < attributes.getLength(); i++) { + Node node = attributes.item(i); + String uri = node.getNamespaceURI(); + if (MERGE_NS_URI.equals(uri)) { + return; + } + } + } + this.context.warning( + "The XML file {} does not contain the XML merge namespace and seems outdated. For details see:\n" + + "https://github.com/devonfw/IDEasy/blob/main/documentation/configurator.adoc#xml-merger", workspaceFile); + } + } 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 95955aa29..41c5d4976 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 @@ -164,7 +164,7 @@ protected Path download(String url, Path target, String downloadFilename, Versio this.context.getFileAccess().download(url, tmpDownloadFile); if (resolvedVersion.toString().equals("latest")) { // Some software vendors violate best-practices and provide the latest version only under a fixed URL. - // Therefore if a newer version of that file gets released, the same URL suddenly leads to a different + // Therefore, if a newer version of that file gets released, the same URL suddenly leads to a different // download file with a newer version and a different checksum. // In order to still support such tools we had to implement this workaround so we cannot move the file in the // download cache for later reuse, cannot verify its checksum and also delete the downloaded file on exit diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomTool.java b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomTool.java deleted file mode 100644 index 47cbb403f..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomTool.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.devonfw.tools.ide.repo; - -import java.util.Set; - -import com.devonfw.tools.ide.os.OperatingSystem; -import com.devonfw.tools.ide.os.SystemArchitecture; -import com.devonfw.tools.ide.os.SystemInfo; -import com.devonfw.tools.ide.url.model.file.UrlDownloadFileMetadata; -import com.devonfw.tools.ide.version.VersionIdentifier; - -/** - * Representation of a {@link CustomTool} from a {@link CustomToolRepository}. - */ -public final class CustomTool implements UrlDownloadFileMetadata { - - private final String tool; - - private final VersionIdentifier version; - - private final boolean osAgnostic; - - private final boolean archAgnostic; - - private final String repositoryUrl; - - private final String url; - - private final String checksum; - - private final OperatingSystem os; - - private final SystemArchitecture arch; - - /** - * The constructor. - * - * @param tool the {@link #getTool() tool}. - * @param versionIdentifier the {@link #getVersion() version}. - * @param osAgnostic the {@link #isOsAgnostic() OS-agnostic flag}. - * @param archAgnostic the {@link #isArchAgnostic() architecture-agnostic flag}. - * @param repositoryUrl the {@link #getRepositoryUrl() repository URL}. - * @param checksum the {@link #getChecksum() checksum}. - * @param systemInfo the {@link SystemInfo}. - */ - public CustomTool(String tool, VersionIdentifier versionIdentifier, boolean osAgnostic, boolean archAgnostic, - String repositoryUrl, String checksum, SystemInfo systemInfo) { - - super(); - this.tool = tool; - this.version = versionIdentifier; - this.osAgnostic = osAgnostic; - this.archAgnostic = archAgnostic; - this.repositoryUrl = repositoryUrl; - String versionString = versionIdentifier.toString(); - int capacity = repositoryUrl.length() + 2 * tool.length() + 2 * versionString.length() + 7; - if (osAgnostic) { - this.os = null; - } else { - this.os = systemInfo.getOs(); - capacity += this.os.toString().length() + 1; - } - if (archAgnostic) { - this.arch = null; - } else { - this.arch = systemInfo.getArchitecture(); - capacity += this.arch.toString().length() + 1; - } - this.checksum = checksum; - StringBuilder sb = new StringBuilder(capacity); - sb.append(this.repositoryUrl); - char last = this.repositoryUrl.charAt(repositoryUrl.length() - 1); - if ((last != '/') && (last != '\\')) { - sb.append('/'); - } - sb.append(tool); - sb.append('/'); - sb.append(versionString); - sb.append('/'); - sb.append(tool); - sb.append('-'); - sb.append(versionString); - if (this.os != null) { - sb.append('-'); - sb.append(this.os); - } - if (this.arch != null) { - sb.append('-'); - sb.append(this.arch); - } - sb.append(".tgz"); - this.url = sb.toString(); - } - - @Override - public String getTool() { - - return this.tool; - } - - @Override - public String getEdition() { - - return this.tool; - } - - @Override - public VersionIdentifier getVersion() { - - return this.version; - } - - /** - * @return {@code true} if {@link OperatingSystem} agnostic, {@code false} otherwise. - */ - public boolean isOsAgnostic() { - - return this.osAgnostic; - } - - /** - * @return {@code true} if {@link SystemArchitecture} agnostic, {@code false} otherwise. - */ - public boolean isArchAgnostic() { - - return this.archAgnostic; - } - - /** - * @return the repository base URL. This may be a typical URL (e.g. "https://host/path") but may also be a path in your file-system (e.g. to a mounted remote - * network drive). - */ - public String getRepositoryUrl() { - - return this.repositoryUrl; - } - - /** - * @return the URL to the download artifact. - */ - public String getUrl() { - - return this.url; - } - - @Override - public String getChecksum() { - - return this.checksum; - } - - @Override - public OperatingSystem getOs() { - - return this.os; - } - - @Override - public SystemArchitecture getArch() { - - return this.arch; - } - - @Override - public Set getUrls() { - - return Set.of(this.url); - } - - @Override - public String toString() { - - return "CustomTool[" + this.tool + ":" + this.version + "@" + this.repositoryUrl + "]"; - } - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolJson.java b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolJson.java new file mode 100644 index 000000000..6bbe9ebc1 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolJson.java @@ -0,0 +1,26 @@ +package com.devonfw.tools.ide.repo; + +import com.devonfw.tools.ide.os.OperatingSystem; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON representation of a single {@link CustomToolMetadata}. + * + * @param name the {@link CustomToolMetadata#getTool() tool name}. + * @param version the {@link CustomToolMetadata#getVersion() tool version}. + * @param osAgnostic {@code true} if {@link OperatingSystem} agnostic, {@code false} otherwise. + * @param archAgnostic {@code true} if {@link com.devonfw.tools.ide.os.SystemArchitecture} agnostic, {@code false} otherwise. + * @param url the overridden {@link CustomToolsJson#url() repository URL} or {@code null} to inherit. + * @see CustomToolsJson#tools() + */ +public record CustomToolJson(String name, String version, @JsonProperty(value = "os-agnostic") boolean osAgnostic, + @JsonProperty(value = "arch-agnostic") boolean archAgnostic, String url) { + + /** + * @return a new {@link CustomToolsJson} having {@link #url()} set to {@code null}. + */ + public CustomToolJson withoutUrl() { + + return new CustomToolJson(name, version, osAgnostic, archAgnostic, null); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolMetadata.java b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolMetadata.java new file mode 100644 index 000000000..085492b27 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolMetadata.java @@ -0,0 +1,119 @@ +package com.devonfw.tools.ide.repo; + +import java.util.Set; + +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 com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Representation of a {@link CustomToolMetadata} from a {@link CustomToolRepository}. + */ +public final class CustomToolMetadata implements UrlDownloadFileMetadata { + + private final String tool; + + private final VersionIdentifier version; + + private final OperatingSystem os; + + private final SystemArchitecture arch; + + private final String url; + + private final String checksum; + + private final String repositoryUrl; + + /** + * The constructor. + * + * @param tool the {@link #getTool() tool}. + * @param versionString the {@link #getVersion() version} as {@link String}. + * @param os the {@link #getOs() OS}. + * @param arch the {@link #getArch() architecture}. + * @param url the {@link #getUrl() download URL}. + * @param checksum the {@link #getChecksum() checksum}. + * @param repositoryUrl the {@link #getRepositoryUrl() repository URL}. + */ + public CustomToolMetadata(String tool, String versionString, OperatingSystem os, SystemArchitecture arch, + String url, String checksum, String repositoryUrl) { + + super(); + this.tool = tool; + this.version = VersionIdentifier.of(versionString); + this.os = os; + this.arch = arch; + this.url = url; + this.checksum = checksum; + this.repositoryUrl = repositoryUrl; + } + + @Override + public String getTool() { + + return this.tool; + } + + @Override + public String getEdition() { + + return this.tool; + } + + @Override + public VersionIdentifier getVersion() { + + return version; + } + + /** + * @return the URL to the download artifact. + */ + public String getUrl() { + + return this.url; + } + + @Override + public String getChecksum() { + + return this.checksum; + } + + @Override + public OperatingSystem getOs() { + + return this.os; + } + + @Override + public SystemArchitecture getArch() { + + return this.arch; + } + + @Override + public Set getUrls() { + + return Set.of(this.url); + } + + /** + * @return the {@link CustomToolsJson#url() repository base URL}. + */ + @JsonIgnore + public String getRepositoryUrl() { + + return this.repositoryUrl; + } + + @Override + public String toString() { + + return "CustomTool[" + this.tool + ":" + this.version + "@" + this.url + "]"; + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepository.java b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepository.java index 5b9ffd13a..bac2a6064 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepository.java +++ b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepository.java @@ -7,12 +7,9 @@ */ public interface CustomToolRepository extends ToolRepository { - /** The filename of the configuration file in the settings for this {@link CustomToolRepository}. */ - String FILE_CUSTOM_TOOLS = "ide-custom-tools.json"; - /** - * @return the {@link Collection} with the {@link CustomTool}s. Will be {@link Collection#isEmpty() empty} if no custom tools are configured. + * @return the {@link Collection} with the {@link CustomToolMetadata}s. Will be {@link Collection#isEmpty() empty} if no custom tools are configured. */ - Collection getTools(); + Collection getTools(); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepositoryImpl.java b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepositoryImpl.java index 0d00d466a..b6cab89d8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepositoryImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepositoryImpl.java @@ -1,9 +1,5 @@ package com.devonfw.tools.ide.repo; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -14,14 +10,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; -import jakarta.json.JsonString; -import jakarta.json.JsonStructure; -import jakarta.json.JsonValue; -import jakarta.json.JsonValue.ValueType; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.url.model.file.UrlDownloadFileMetadata; @@ -36,24 +24,24 @@ public class CustomToolRepositoryImpl extends AbstractToolRepository implements private final String id; - private final Map toolsMap; + private final Map toolsMap; - private final Collection tools; + private final Collection tools; /** * The constructor. * * @param context the owning {@link IdeContext}. - * @param tools the {@link CustomTool}s. + * @param tools the {@link CustomToolMetadata}s. */ - public CustomToolRepositoryImpl(IdeContext context, Collection tools) { + public CustomToolRepositoryImpl(IdeContext context, Collection tools) { super(context); this.toolsMap = new HashMap<>(tools.size()); String repoId = null; - for (CustomTool tool : tools) { + for (CustomToolMetadata tool : tools) { String name = tool.getTool(); - CustomTool duplicate = this.toolsMap.put(name, tool); + CustomToolMetadata duplicate = this.toolsMap.put(name, tool); if (duplicate != null) { throw new IllegalStateException("Duplicate custom tool '" + name + "'!"); } @@ -76,7 +64,7 @@ private static String computeId(String url) { id = id.substring(schemaIndex + 3); // remove schema like "https://" id = URLDecoder.decode(id, StandardCharsets.UTF_8); } - id.replace('\\', '/').replace("//", "/"); // normalize slashes + id = id.replace('\\', '/').replace("//", "/"); // normalize slashes if (id.startsWith("/")) { id = id.substring(1); } @@ -111,7 +99,7 @@ public String getId() { @Override protected UrlDownloadFileMetadata getMetadata(String tool, String edition, VersionIdentifier version) { - CustomTool customTool = getCustomTool(tool); + CustomToolMetadata customTool = getCustomTool(tool); if (!version.equals(customTool.getVersion())) { throw new IllegalArgumentException("Undefined version '" + version + "' for custom tool '" + tool + "' - expected version '" + customTool.getVersion() + "'!"); @@ -122,8 +110,8 @@ protected UrlDownloadFileMetadata getMetadata(String tool, String edition, Versi return customTool; } - private CustomTool getCustomTool(String tool) { - CustomTool customTool = this.toolsMap.get(tool); + private CustomToolMetadata getCustomTool(String tool) { + CustomToolMetadata customTool = this.toolsMap.get(tool); if (customTool == null) { throw new IllegalArgumentException("Undefined custom tool '" + tool + "'!"); } @@ -133,16 +121,16 @@ private CustomTool getCustomTool(String tool) { @Override public VersionIdentifier resolveVersion(String tool, String edition, GenericVersionRange version) { - CustomTool customTool = getCustomTool(tool); - VersionIdentifier customToolVerstion = customTool.getVersion(); - if (!version.contains(customToolVerstion)) { + CustomToolMetadata customTool = getCustomTool(tool); + VersionIdentifier customToolVersion = customTool.getVersion(); + if (!version.contains(customToolVersion)) { throw new IllegalStateException(customTool + " does not satisfy version to install " + version); } - return customToolVerstion; + return customToolVersion; } @Override - public Collection getTools() { + public Collection getTools() { return this.tools; } @@ -160,109 +148,18 @@ public Collection findDependencies(String tool, String edition, public static CustomToolRepository of(IdeContext context) { Path settingsPath = context.getSettingsPath(); - Path customToolsJson = null; + Path customToolsJsonFile = null; if (settingsPath != null) { - customToolsJson = settingsPath.resolve(FILE_CUSTOM_TOOLS); + customToolsJsonFile = settingsPath.resolve(IdeContext.FILE_CUSTOM_TOOLS); } - List tools = new ArrayList<>(); - if ((customToolsJson != null) && Files.exists(customToolsJson)) { - try (InputStream in = Files.newInputStream(customToolsJson); - Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { - - JsonReader jsonReader = Json.createReader(new BufferedReader(reader)); - JsonStructure json = jsonReader.read(); - JsonObject jsonRoot = requireObject(json); - String defaultUrl = getString(jsonRoot, "url", ""); - JsonArray jsonTools = requireArray(jsonRoot.get("tools")); - for (JsonValue jsonTool : jsonTools) { - JsonObject jsonToolObject = requireObject(jsonTool); - String name = getString(jsonToolObject, "name"); - String version = getString(jsonToolObject, "version"); - String url = getString(jsonToolObject, "url", defaultUrl); - boolean osAgnostic = getBoolean(jsonToolObject, "os-agnostic", Boolean.FALSE); - boolean archAgnostic = getBoolean(jsonToolObject, "arch-agnostic", Boolean.TRUE); - if (url.isEmpty()) { - throw new IllegalStateException("Missing 'url' property for tool '" + name + "'!"); - } - // TODO - String checksum = null; - CustomTool customTool = new CustomTool(name, VersionIdentifier.of(version), osAgnostic, archAgnostic, url, - checksum, context.getSystemInfo()); - tools.add(customTool); - } - } catch (Exception e) { - throw new IllegalStateException("Failed to read JSON from " + customToolsJson, e); - } - } - return new CustomToolRepositoryImpl(context, tools); - } - - private static boolean getBoolean(JsonObject json, String property, Boolean defaultValue) { - - JsonValue value = json.get(property); - if (value == null) { - if (defaultValue == null) { - throw new IllegalArgumentException("Missing string property '" + property + "' in JSON: " + json); - } - return defaultValue.booleanValue(); - } - ValueType valueType = value.getValueType(); - if (valueType == ValueType.TRUE) { - return true; - } else if (valueType == ValueType.FALSE) { - return false; + List tools; + if ((customToolsJsonFile != null) && Files.exists(customToolsJsonFile)) { + CustomToolsJson customToolsJson = CustomToolsJsonMapper.loadJson(customToolsJsonFile); + tools = CustomToolsJsonMapper.convert(customToolsJson, context); } else { - throw new IllegalStateException("Expected value type boolean but found " + valueType + " for JSON: " + json); - } - } - - private static String getString(JsonObject json, String property) { - - return getString(json, property, null); - } - - private static String getString(JsonObject json, String property, String defaultValue) { - - JsonValue value = json.get(property); - if (value == null) { - if (defaultValue == null) { - throw new IllegalArgumentException("Missing string property '" + property + "' in JSON: " + json); - } - return defaultValue; - } - require(value, ValueType.STRING); - return ((JsonString) value).getString(); - } - - /** - * @param json the {@link JsonValue} to check. - */ - private static JsonObject requireObject(JsonValue json) { - - require(json, ValueType.OBJECT); - return (JsonObject) json; - } - - /** - * @param json the {@link JsonValue} to check. - */ - private static JsonArray requireArray(JsonValue json) { - - require(json, ValueType.ARRAY); - return (JsonArray) json; - } - - /** - * @param json the {@link JsonValue} to check. - * @param type the expected {@link ValueType}. - */ - private static void require(JsonValue json, ValueType type) { - - ValueType actualType = json.getValueType(); - if (actualType != type) { - throw new IllegalStateException( - "Expected value type " + type + " but found " + actualType + " for JSON: " + json); + tools = new ArrayList<>(); } + return new CustomToolRepositoryImpl(context, tools); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolsJson.java b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolsJson.java new file mode 100644 index 000000000..7f8a8b19d --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolsJson.java @@ -0,0 +1,14 @@ +package com.devonfw.tools.ide.repo; + +import java.util.List; + +/** + * {@link CustomToolsJson} for the ide-custom-tools.json file. + * + * @param url the repository base URL. This may be a typical URL (e.g. "https://host/path") but may also be a path in your file-system (e.g. to a mounted + * remote network drive). + * @param tools the {@link List} of {@link CustomToolJson}. + */ +public record CustomToolsJson(String url, List tools) { + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolsJsonMapper.java b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolsJsonMapper.java new file mode 100644 index 000000000..e60100876 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolsJsonMapper.java @@ -0,0 +1,186 @@ +package com.devonfw.tools.ide.repo; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.VariableLine; +import com.devonfw.tools.ide.json.JsonMapping; +import com.devonfw.tools.ide.os.OperatingSystem; +import com.devonfw.tools.ide.os.SystemArchitecture; +import com.devonfw.tools.ide.os.SystemInfo; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Mapper of {@link CustomToolsJson} from/to JSON. + */ +public class CustomToolsJsonMapper { + + private static final ObjectMapper MAPPER = JsonMapping.create(); + + /** + * @param customTools the {@link CustomToolsJson} to save. + * @param path the {@link Path} of the file where to save as JSON. + */ + public static void saveJson(CustomToolsJson customTools, Path path) { + + try (BufferedWriter writer = Files.newBufferedWriter(path)) { + MAPPER.writerWithDefaultPrettyPrinter().writeValue(writer, customTools); + } catch (Exception e) { + throw new IllegalStateException("Failed to save file " + path, e); + } + } + + /** + * @param path the {@link Path} of the JSON file to load. + * @return the parsed {@link CustomToolsJson} or {@code null} if the {@link Path} is not an existing file. + */ + public static CustomToolsJson loadJson(Path path) { + CustomToolsJson customToolsJson = null; + if (Files.isRegularFile(path)) { + try (BufferedReader reader = Files.newBufferedReader(path)) { + customToolsJson = MAPPER.readValue(reader, CustomToolsJson.class); + } catch (Exception e) { + throw new IllegalStateException("Failed to load " + path, e); + } + } + return customToolsJson; + } + + /** + * @param customTools the {@link CustomToolsJson} to convert. + * @param context the {@link IdeContext}. + * @return the converted {@link List} of {@link CustomToolMetadata}. + */ + public static List convert(CustomToolsJson customTools, IdeContext context) { + + String repositoryUrl = customTools.url(); + List tools = customTools.tools(); + List result = new ArrayList<>(tools.size()); + for (CustomToolJson customTool : tools) { + result.add(convert(customTool, repositoryUrl, context)); + } + return result; + } + + private static CustomToolMetadata convert(CustomToolJson customTool, String repositoryUrl, IdeContext context) { + + String tool = customTool.name(); + String version = customTool.version(); + SystemInfo systemInfo = context.getSystemInfo(); + String repoUrl = customTool.url(); + if ((repoUrl == null) || repoUrl.isEmpty()) { + repoUrl = repositoryUrl; + } + int capacity = repoUrl.length() + 2 * tool.length() + 2 * version.length() + 7; + OperatingSystem os; + if (customTool.osAgnostic()) { + os = null; + } else { + os = systemInfo.getOs(); + capacity += os.toString().length() + 1; + } + SystemArchitecture arch; + if (customTool.archAgnostic()) { + arch = null; + } else { + arch = systemInfo.getArchitecture(); + capacity += arch.toString().length() + 1; + } + StringBuilder sb = new StringBuilder(capacity); + sb.append(repoUrl); + char last = repoUrl.charAt(repoUrl.length() - 1); + if ((last != '/') && (last != '\\')) { + sb.append('/'); + } + sb.append(tool); + sb.append('/'); + sb.append(version); + sb.append('/'); + sb.append(tool); + sb.append('-'); + sb.append(version); + if (os != null) { + sb.append('-'); + sb.append(os); + } + if (arch != null) { + sb.append('-'); + sb.append(arch); + } + sb.append(".tgz"); + String url = sb.toString(); + return new CustomToolMetadata(tool, version, os, arch, url, null, repoUrl); + } + + /** + * Retrieves custom tools from a devonfw-ide legacy config. + * + * @param customToolsContent String of custom tools + * @param context the {@link IdeContext}. + * @return {@link CustomToolsJson}. + */ + public static CustomToolsJson parseCustomToolsFromLegacyConfig(String customToolsContent, IdeContext context) { + List customToolEntries = VariableLine.parseArray(customToolsContent); + if (customToolEntries.isEmpty()) { + return null; + } + List customTools = new ArrayList<>(customToolEntries.size()); + String defaultUrl = null; + for (String customToolConfig : customToolEntries) { + CustomToolJson customToolJson = parseCustomToolFromLegacyConfig(customToolConfig); + if (customToolJson == null) { + context.warning("Invalid custom tool entry: {}", customToolConfig); + } else { + String url = customToolJson.url(); + if (defaultUrl == null) { + if ((url == null) || url.isEmpty()) { + context.warning("First custom tool entry has no URL specified: {}", customToolConfig); + } else { + defaultUrl = url; + customToolJson = customToolJson.withoutUrl(); + } + } else if (defaultUrl.equals(url)) { + customToolJson = customToolJson.withoutUrl(); + } + customTools.add(customToolJson); + } + } + if (customTools.isEmpty() || (defaultUrl == null)) { + return null; + } + return new CustomToolsJson(defaultUrl, customTools); + } + + private static CustomToolJson parseCustomToolFromLegacyConfig(String customToolConfig) { + int firstColon = customToolConfig.indexOf(":"); + if (firstColon < 0) { + return null; + } + String toolName = customToolConfig.substring(0, firstColon); + int secondColon = customToolConfig.indexOf(":", firstColon + 1); + if (secondColon < 0) { + return null; + } + String version = customToolConfig.substring(firstColon + 1, secondColon); + int thirdColon = customToolConfig.indexOf(":", secondColon + 1); + boolean osAgnostic = false; + boolean archAgnostic = false; + String url = null; + if (thirdColon > 0) { + if (customToolConfig.substring(secondColon + 1, thirdColon).equals("all")) { + osAgnostic = true; + archAgnostic = true; + url = customToolConfig.substring(thirdColon + 1); + } else { + url = customToolConfig.substring(secondColon + 1); + } + } + return new CustomToolJson(toolName, version, osAgnostic, archAgnostic, url); + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/CustomToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/CustomToolCommandlet.java index acb4cb1ad..3a0be1d2f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/CustomToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/CustomToolCommandlet.java @@ -1,17 +1,17 @@ package com.devonfw.tools.ide.tool; import com.devonfw.tools.ide.context.IdeContext; -import com.devonfw.tools.ide.repo.CustomTool; +import com.devonfw.tools.ide.repo.CustomToolMetadata; import com.devonfw.tools.ide.version.VersionIdentifier; /** - * {@link LocalToolCommandlet} for a {@link CustomTool}. + * {@link LocalToolCommandlet} for a {@link CustomToolMetadata}. */ public class CustomToolCommandlet extends LocalToolCommandlet { - private CustomTool customTool; + private CustomToolMetadata customTool; - public CustomToolCommandlet(IdeContext context, CustomTool customTool) { + public CustomToolCommandlet(IdeContext context, CustomToolMetadata customTool) { super(context, customTool.getTool(), null); this.customTool = customTool; diff --git a/cli/src/main/java/com/devonfw/tools/ide/util/DateTimeUtil.java b/cli/src/main/java/com/devonfw/tools/ide/util/DateTimeUtil.java index 0fd836d9a..5dc3116fa 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/util/DateTimeUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/util/DateTimeUtil.java @@ -11,7 +11,10 @@ */ public final class DateTimeUtil { - private static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder().appendPattern("YYYY-MM-dd") + private static final DateTimeFormatter DATE_FORMATTER_PATH = new DateTimeFormatterBuilder().appendPattern("YYYY/MM/dd") + .toFormatter(); + + private static final DateTimeFormatter DATE_FORMATTER_NAME = new DateTimeFormatterBuilder().appendPattern("YYYY-MM-dd") .toFormatter(); private static final DateTimeFormatter TIME_FORMATTER = new DateTimeFormatterBuilder().appendPattern("HH-mm-ss") @@ -66,11 +69,16 @@ public static Integer compareDuration(Instant start, Instant end, Duration durat /** * @param temporal the {@link LocalDateTime} to format as date. - * @return the {@link LocalDateTime} formatted as date in the format YYYY-MM-dd. + * @param dirs {@code true} to use "/" as separator to create subfolders per year, month, and date, {@code false} otherwise. + * @return the {@link LocalDateTime} formatted as date in the format YYYY-MM-dd or YYYY/MM/dd. */ - public static String formatDate(LocalDateTime temporal) { + public static String formatDate(LocalDateTime temporal, boolean dirs) { - return temporal.format(DATE_FORMATTER); + if (dirs) { + return temporal.format(DATE_FORMATTER_PATH); + } else { + return temporal.format(DATE_FORMATTER_NAME); + } } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java b/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java index c88080231..73cd6d995 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java +++ b/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java @@ -86,6 +86,9 @@ public interface IdeVariables { VariableDefinitionBoolean IDE_VARIABLE_SYNTAX_LEGACY_SUPPORT_ENABLED = new VariableDefinitionBoolean("IDE_VARIABLE_SYNTAX_LEGACY_SUPPORT_ENABLED", null, c -> Boolean.TRUE); + /** {@link VariableDefinition} for {@link com.devonfw.tools.ide.context.IdeContext#getProjectName() DEVON_IDE_CUSTOM_TOOLS}. */ + VariableDefinitionString DEVON_IDE_CUSTOM_TOOLS = new VariableDefinitionString("DEVON_IDE_CUSTOM_TOOLS"); + /** A {@link Collection} with all pre-defined {@link VariableDefinition}s. */ Collection> VARIABLES = List.of(PATH, HOME, WORKSPACE_PATH, IDE_HOME, IDE_ROOT, WORKSPACE, IDE_TOOLS, CREATE_START_SCRIPTS, IDE_MIN_VERSION, MVN_VERSION, M2_REPO, DOCKER_EDITION, MVN_BUILD_OPTS, NPM_BUILD_OPTS, GRADLE_BUILD_OPTS, YARN_BUILD_OPTS, JASYPT_OPTS, MAVEN_ARGS, diff --git a/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinitionStringList.java b/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinitionStringList.java index e1b61f6cb..56055cd42 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinitionStringList.java +++ b/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinitionStringList.java @@ -1,6 +1,5 @@ package com.devonfw.tools.ide.variable; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -61,7 +60,7 @@ public VariableDefinitionStringList(String name, String legacyName, super(name, legacyName, defaultValueFactory, forceDefaultValue); } - @SuppressWarnings( { "unchecked", "rawtypes" }) + @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public Class> getValueType() { @@ -74,17 +73,7 @@ public List fromString(String value, IdeContext context) { if (value.isEmpty()) { return Collections.emptyList(); } - String csv = value; - String separator = ","; - if (isBashArray(value)) { - csv = value.substring(1, value.length() - 1); - separator = " "; - } - String[] items = csv.split(separator); - List list = new ArrayList<>(items.length); - for (String item : items) { - list.add(item.trim()); - } + List list = VariableLine.parseArray(value); list = Collections.unmodifiableList(list); return list; } diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 42486803f..9ee1535bc 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -108,12 +108,15 @@ 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-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.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). cmd.vscode.detail=Visual Studio Code (VS Code) is a popular code editor developed by Microsoft. Detailed documentation can be found at https://code.visualstudio.com/docs/ commandlets=Available commandlets: opt.--batch=enable batch mode (non-interactive). +opt.--code=clone given code repository containing a settings folder into workspaces so that settings can be committed alongside code changes. opt.--debug=enable debug logging. opt.--force=enable force mode. opt.--locale=the locale (e.g. '--locale=de' for German language). diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index 3e7af8e8c..a73ea56e3 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -108,12 +108,15 @@ 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-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.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). cmd.vscode.detail=Visual Studio Code (VS Code) ist ein beliebter Code-Editor, der von Microsoft entwickelt wurde. Detaillierte Dokumentation ist zu finden unter https://code.visualstudio.com/docs/ commandlets=Verfügbare Kommandos: opt.--batch=Aktiviert den Batch-Modus (nicht-interaktive Stapelverarbeitung). +opt.--code=Git-Repository sowohl als Code- als auch als Settings-Repository verwenden. opt.--debug=Aktiviert Debug-Ausgaben (Fehleranalyse). opt.--force=Aktiviert den Force-Modus (Erzwingen). opt.--locale=Die Spracheinstellungen (z.B. 'en' für Englisch). diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java index 02b1d599f..ad480c094 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java @@ -29,7 +29,7 @@ public class HelpCommandletTest extends AbstractIdeContextTest { * Test of {@link HelpCommandlet} does not require home. */ @Test - public void testThatHomeIsNotReqired() { + public void testThatHomeIsNotRequired() { // arrange IdeContext context = IdeTestContextMock.get(); diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpgradeSettingsCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpgradeSettingsCommandletTest.java new file mode 100644 index 000000000..23b496aa7 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpgradeSettingsCommandletTest.java @@ -0,0 +1,102 @@ +package com.devonfw.tools.ide.commandlet; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.repo.CustomToolJson; +import com.devonfw.tools.ide.repo.CustomToolsJson; +import com.devonfw.tools.ide.repo.CustomToolsJsonMapper; + +/** + * Integration test of {@link UpgradeSettingsCommandlet} . + */ +public class UpgradeSettingsCommandletTest extends AbstractIdeContextTest { + + private static final String PROJECT_UPGRADE_SETTINGS = "upgrade-settings"; + private final IdeTestContext context = newContext(PROJECT_UPGRADE_SETTINGS); + private static final Path UPGRADE_SETTINGS_PATH = TEST_PROJECTS_COPY.resolve(PROJECT_UPGRADE_SETTINGS).resolve("project"); + + /** + * Test of {@link UpgradeSettingsCommandlet}. + * + * @throws Exception on error. + */ + @Test + public void testUpdateSettings() throws Exception { + // arrange + UpgradeSettingsCommandlet upgradeSettingsCommandlet = new UpgradeSettingsCommandlet(context); + // act + upgradeSettingsCommandlet.run(); + // assert + verifyUpdateLegacyFolders(); + verifyUpdateProperties(); + verifyUpdateWorkspaceTemplates(); + } + + /** + * @throws Exception on error. + */ + private void verifyUpdateProperties() throws Exception { + + assertThat(UPGRADE_SETTINGS_PATH.resolve("home/.ide/ide.properties")).exists(); + assertThat(UPGRADE_SETTINGS_PATH.resolve("settings/ide.properties")).exists().content().contains("INTELLIJ_EDITION=ultimate") + .doesNotContain("INTELLIJ_EDITION_TYPE").contains("IDE_VARIABLE_SYNTAX_LEGACY_SUPPORT_ENABLED=false"); + assertThat(UPGRADE_SETTINGS_PATH.resolve("workspaces/main/ide.properties")).exists(); + //assert that file content was changed + assertThat(UPGRADE_SETTINGS_PATH.resolve("conf/ide.properties")).exists().content().contains("MVN_VERSION=test"); + + // devon.properties have been deleted (moved to backup) + assertThat(UPGRADE_SETTINGS_PATH.resolve("home/devon.properties")).doesNotExist(); + assertThat(UPGRADE_SETTINGS_PATH.resolve("settings/devon.properties")).doesNotExist(); + assertThat(UPGRADE_SETTINGS_PATH.resolve("workspaces/main/devon.properties")).doesNotExist(); + assertThat(UPGRADE_SETTINGS_PATH.resolve("conf/devon.properties")).doesNotExist(); + verifyCustomToolsJson(); + } + + private void verifyCustomToolsJson() throws Exception { + // arrange + UpgradeSettingsCommandlet upgradeSettingsCommandlet = new UpgradeSettingsCommandlet(context); + // act + upgradeSettingsCommandlet.run(); + // assert + + Path customToolsJsonFile = UPGRADE_SETTINGS_PATH.resolve("settings").resolve(IdeContext.FILE_CUSTOM_TOOLS); + // assert that ide-custom-tools.json exists + assertThat(customToolsJsonFile).exists(); + CustomToolsJson customToolsJson = CustomToolsJsonMapper.loadJson(customToolsJsonFile); + //assert that ide-custom-tools.json has the correct content + assertThat(customToolsJson.url()).isEqualTo("https://host.tld/projects/my-project"); + assertThat(customToolsJson.tools()).containsExactly(new CustomToolJson("jboss-eap", "7.1.4.GA", true, true, null), + new CustomToolJson("firefox", "70.0.1", false, false, null)); + } + + private void verifyUpdateLegacyFolders() { + assertThat(UPGRADE_SETTINGS_PATH.resolve("settings/repositories/IDEasy.properties")).exists(); + assertThat(UPGRADE_SETTINGS_PATH.resolve("settings/templates/conf/ide.properties")).exists(); + assertThat(UPGRADE_SETTINGS_PATH.resolve("settings/templates/conf/mvn/settings.xml")).exists(); + } + + private void verifyUpdateWorkspaceTemplates() { + // arrange + UpgradeSettingsCommandlet upgradeSettingsCommandlet = new UpgradeSettingsCommandlet(context); + // act + upgradeSettingsCommandlet.run(); + //assert + assertThat(UPGRADE_SETTINGS_PATH.resolve("settings/workspace/testVariableSyntax.txt")).exists().content().contains("$[IDE_HOME]").contains("$[MVN_VERSION]") + .contains("$[IDE_HOME]/conf/mvn/settings.xml").doesNotContain("${IDE_HOME}").doesNotContain("${MVN_VERSION}"); + verifyLoggingOfXmlFiles(); + } + + private void verifyLoggingOfXmlFiles() { + Path workspace = UPGRADE_SETTINGS_PATH.resolve(IdeContext.FOLDER_SETTINGS).resolve("intellij").resolve(IdeContext.FOLDER_WORKSPACE).resolve("TestXml.xml") + .toAbsolutePath(); + //assert + assertThat(context).logAtWarning().hasMessage( + "The XML file " + workspace + " does not contain the XML merge namespace and seems outdated. For details see:\n" + + "https://github.com/devonfw/IDEasy/blob/main/documentation/configurator.adoc#xml-merger"); + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java index 39a0f0016..943145513 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java @@ -28,7 +28,7 @@ public abstract class AbstractIdeContextTest extends Assertions { protected static final Path TEST_PROJECTS = TEST_RESOURCES.resolve("ide-projects"); // will not use eclipse-target like done in maven via eclipse profile... - private static final Path TEST_PROJECTS_COPY = Path.of("target/test-projects"); + protected static final Path TEST_PROJECTS_COPY = Path.of("target/test-projects"); /** Chunk size to use for progress bars **/ private static final int CHUNK_SIZE = 1024; diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 014655d9b..3afb4ba52 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -73,6 +73,12 @@ public boolean isRepositoryUpdateAvailable(Path repository) { return false; } + @Override + public boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath) { + + return false; + } + @Override public String determineCurrentBranch(Path repository) { @@ -84,4 +90,10 @@ public String determineRemote(Path repository) { return "origin"; } + + + @Override + public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) { + + } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java b/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java index bdc452214..bc85c9ed9 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java @@ -50,6 +50,7 @@ public void testConfigurator(@TempDir Path workspaceDir) throws Exception { // arrange IdeContext context = newContext(PROJECT_BASIC, null, false); DirectoryMerger merger = context.getWorkspaceMerger(); + PropertiesMerger propertiesMerger = new PropertiesMerger(context); Path templates = Path.of("src/test/resources/templates"); Path setup = templates.resolve(IdeContext.FOLDER_SETUP); Path update = templates.resolve(IdeContext.FOLDER_UPDATE); @@ -62,7 +63,7 @@ public void testConfigurator(@TempDir Path workspaceDir) throws Exception { // assert Path mainPrefsFile = workspaceDir.resolve("main.prefs"); - Properties mainPrefs = PropertiesMerger.load(mainPrefsFile); + Properties mainPrefs = propertiesMerger.load(mainPrefsFile); assertThat(mainPrefs).containsOnly(JAVA_VERSION, JAVA_HOME, THEME, UI); Path jsonFolder = workspaceDir.resolve("json"); assertThat(jsonFolder).isDirectory(); @@ -90,7 +91,7 @@ public void testConfigurator(@TempDir Path workspaceDir) throws Exception { Path configFolder = workspaceDir.resolve("config"); assertThat(configFolder).isDirectory(); Path indentFile = configFolder.resolve("indent.properties"); - Properties indent = PropertiesMerger.load(indentFile); + Properties indent = propertiesMerger.load(indentFile); assertThat(indent).containsOnly(INDENTATION); assertThat(configFolder.resolve("layout.xml")).hasContent(""" @@ -101,7 +102,7 @@ public void testConfigurator(@TempDir Path workspaceDir) throws Exception { console ${IDE_HOME} - """.replace("${IDE_HOME}", IDE_HOME)); + """.replace("${IDE_HOME}", IDE_HOME)); // and arrange EDITOR.apply(mainPrefs); @@ -109,13 +110,13 @@ public void testConfigurator(@TempDir Path workspaceDir) throws Exception { UI_HACKED.apply(mainPrefs); THEME_HACKED.apply(mainPrefs); INDENTATION_HACKED.apply(mainPrefs); - PropertiesMerger.save(mainPrefs, mainPrefsFile); + propertiesMerger.save(mainPrefs, mainPrefsFile); // act merger.merge(setup, update, context.getVariables(), workspaceDir); // assert - mainPrefs = PropertiesMerger.load(mainPrefsFile); + mainPrefs = propertiesMerger.load(mainPrefsFile); assertThat(mainPrefs).containsOnly(JAVA_VERSION, JAVA_HOME, THEME_HACKED, UI_HACKED, EDITOR, INDENTATION_HACKED); assertThat(namePath).hasContent("project - main\ntest"); diff --git a/cli/src/test/java/com/devonfw/tools/ide/repo/CustomToolMetadataTest.java b/cli/src/test/java/com/devonfw/tools/ide/repo/CustomToolMetadataTest.java new file mode 100644 index 000000000..d98183157 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/repo/CustomToolMetadataTest.java @@ -0,0 +1,68 @@ +package com.devonfw.tools.ide.repo; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.os.OperatingSystem; +import com.devonfw.tools.ide.os.SystemArchitecture; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * Test of {@link CustomToolMetadata}. + */ +public class CustomToolMetadataTest extends Assertions { + + /** + * Test of {@link CustomToolMetadata}. + */ + @Test + public void testAgnostic() { + + // arrange + String name = "jboss-eap"; + String version = "7.4.5.GA"; + String repositoryUrl = "https://host.domain.tld:8443/folder/repo/"; + String url = repositoryUrl + "jboss-eap/7.4.5.GA/jboss-eap-7.4.5.GA.tgz"; + String checksum = "4711"; + OperatingSystem os = null; + SystemArchitecture arch = null; + // act + CustomToolMetadata tool = new CustomToolMetadata(name, version, os, arch, url, checksum, repositoryUrl); + // assert + assertThat(tool.getTool()).isEqualTo(name); + assertThat(tool.getVersion()).isEqualTo(VersionIdentifier.of(version)); + assertThat(tool.getOs()).isEqualTo(os); + assertThat(tool.getArch()).isEqualTo(arch); + assertThat(tool.getUrl()).isEqualTo(url); + assertThat(tool.getChecksum()).isEqualTo(checksum); + assertThat(tool.getRepositoryUrl()).isEqualTo(repositoryUrl); + } + + /** + * Test of {@link CustomToolMetadata}. + */ + @Test + public void testSpecific() { + + // arrange + String name = "firefox"; + String version = "70.0.1"; + String repositoryUrl = "https://host.domain.tld:8443/folder/repo"; + String url = repositoryUrl + "/firefox/70.0.1/firefox-70.0.1-windows.tgz"; + String checksum = "4711"; + OperatingSystem os = OperatingSystem.MAC; + SystemArchitecture arch = SystemArchitecture.ARM64; + // act + CustomToolMetadata tool = new CustomToolMetadata(name, version, os, arch, url, checksum, repositoryUrl); + // assert + assertThat(tool.getTool()).isEqualTo(name); + assertThat(tool.getVersion()).isEqualTo(VersionIdentifier.of(version)); + assertThat(tool.getOs()).isEqualTo(os); + assertThat(tool.getArch()).isEqualTo(arch); + assertThat(tool.getUrl()).isEqualTo(url); + assertThat(tool.getChecksum()).isEqualTo(checksum); + assertThat(tool.getRepositoryUrl()).isEqualTo(repositoryUrl); + } + + +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/repo/CustomToolTest.java b/cli/src/test/java/com/devonfw/tools/ide/repo/CustomToolTest.java deleted file mode 100644 index d5ef2df71..000000000 --- a/cli/src/test/java/com/devonfw/tools/ide/repo/CustomToolTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.devonfw.tools.ide.repo; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -import com.devonfw.tools.ide.os.SystemInfoMock; -import com.devonfw.tools.ide.version.VersionIdentifier; - -/** - * Test of {@link CustomTool}. - */ -public class CustomToolTest extends Assertions { - - /** - * Test of {@link CustomTool}. - */ - @Test - public void testAgnostic() { - - // arrange - String name = "jboss-eap"; - VersionIdentifier version = VersionIdentifier.of("7.4.5.GA"); - String repositoryUrl = "https://host.domain.tld:8443/folder/repo"; - String checksum = "4711"; - boolean osAgnostic = true; - boolean archAgnostic = true; - // act - CustomTool tool = new CustomTool(name, version, osAgnostic, archAgnostic, repositoryUrl, checksum, null); - // assert - assertThat(tool.getTool()).isEqualTo(name); - assertThat(tool.getVersion()).isSameAs(version); - assertThat(tool.isOsAgnostic()).isEqualTo(osAgnostic); - assertThat(tool.isArchAgnostic()).isEqualTo(archAgnostic); - assertThat(tool.getRepositoryUrl()).isEqualTo(repositoryUrl); - assertThat(tool.getUrl()).isEqualTo( - "https://host.domain.tld:8443/folder/repo/jboss-eap/7.4.5.GA/jboss-eap-7.4.5.GA.tgz"); - assertThat(tool.getChecksum()).isEqualTo(checksum); - } - - /** - * Test of {@link CustomTool}. - */ - @Test - public void testSpecific() { - - // arrange - String name = "firefox"; - VersionIdentifier version = VersionIdentifier.of("70.0.1"); - String repositoryUrl = "https://host.domain.tld:8443/folder/repo"; - String checksum = "4711"; - boolean osAgnostic = false; - boolean archAgnostic = true; - // act - CustomTool tool = new CustomTool(name, version, osAgnostic, archAgnostic, repositoryUrl, checksum, - SystemInfoMock.WINDOWS_X64); - // assert - assertThat(tool.getTool()).isEqualTo(name); - assertThat(tool.getVersion()).isSameAs(version); - assertThat(tool.isOsAgnostic()).isEqualTo(osAgnostic); - assertThat(tool.isArchAgnostic()).isEqualTo(archAgnostic); - assertThat(tool.getRepositoryUrl()).isEqualTo(repositoryUrl); - assertThat(tool.getUrl()).isEqualTo( - "https://host.domain.tld:8443/folder/repo/firefox/70.0.1/firefox-70.0.1-windows.tgz"); - assertThat(tool.getChecksum()).isEqualTo(checksum); - } - -} diff --git a/cli/src/test/java/com/devonfw/tools/ide/repo/CustomToolsJsonMapperTest.java b/cli/src/test/java/com/devonfw/tools/ide/repo/CustomToolsJsonMapperTest.java new file mode 100644 index 000000000..97068c515 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/repo/CustomToolsJsonMapperTest.java @@ -0,0 +1,124 @@ +package com.devonfw.tools.ide.repo; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.context.AbstractIdeTestContext; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.context.IdeSlf4jContext; +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.os.SystemInfoMock; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * Test of {@link CustomToolsJsonMapper}. + */ +public class CustomToolsJsonMapperTest extends Assertions { + + @Test + public void testReadCustomToolsFromJson() { + // arrange + Path testPath = Path.of("src/test/resources/customtools"); + // act + CustomToolsJson customToolsJson = CustomToolsJsonMapper.loadJson(testPath.resolve("ide-custom-tools.json")); + // assert + assertThat(customToolsJson.url()).isEqualTo("https://some-file-server.company.com/projects/my-project"); + // assert that custom tools content matches to json file + assertThat(customToolsJson.tools()).containsExactly(new CustomToolJson("jboss-eap", "7.1.4.GA", true, true, + null), + new CustomToolJson("firefox", "70.0.1", false, false, + "https://some-file-server.company.com/projects/my-project2")); + } + + @Test + public void testReadCustomToolsFromLegacyConfig() { + // arrange + IdeContext context = new IdeTestContext(); + String legacyProperties = "(jboss-eap:7.1.4.GA:all:https://host.tld/projects/my-project firefox:70.0.1:all:)"; + // act + CustomToolsJson customToolsJson = CustomToolsJsonMapper.parseCustomToolsFromLegacyConfig(legacyProperties, context); + // assert + assertThat(customToolsJson.url()).isEqualTo("https://host.tld/projects/my-project"); + assertThat(customToolsJson.tools()).containsExactly(new CustomToolJson("jboss-eap", "7.1.4.GA", true, true, + null), + new CustomToolJson("firefox", "70.0.1", true, true, "")); + } + + @Test + public void testReadEmptyCustomToolsFromLegacyConfig() { + // arrange + IdeContext context = new IdeTestContext(); + String legacyProperties = "()"; + // act + CustomToolsJson customToolsJson = CustomToolsJsonMapper.parseCustomToolsFromLegacyConfig(legacyProperties, context); + // assert + assertThat(customToolsJson).isNull(); + } + + @Test + public void testReadFaultyCustomToolsFromLegacyConfig() { + // arrange + IdeContext context = new IdeTestContext(); + String legacyProperties = "(jboss-eap:7.1.4.GA:all)"; + // act + CustomToolsJson customToolsJson = CustomToolsJsonMapper.parseCustomToolsFromLegacyConfig(legacyProperties, context); + // assert + assertThat(customToolsJson).isNull(); + } + + /** + * Tests the convert of a {@link CustomToolsJson} with different os and arch agnostic settings to a proper {@link CustomToolMetadata}. + */ + @Test + public void testProperConvertFromCustomToolsJsonToCustomToolMetaData() { + + // arrange + AbstractIdeTestContext context = new IdeSlf4jContext(Path.of("")); + context.setSystemInfo(SystemInfoMock.LINUX_X64); + String name = "jboss-eap"; + String version = "7.4.5.GA"; + String repositoryUrl = "https://host.domain.tld:8443/folder/repo/"; + String url = repositoryUrl + "jboss-eap/7.4.5.GA/jboss-eap-7.4.5.GA.tgz"; + OperatingSystem os = null; + SystemArchitecture arch = null; + + String name1 = "firefox"; + String version1 = "70.0.1"; + String repositoryUrl1 = "https://host.domain.tld:8443/folder/repo/"; + String checkOsArchUrl = repositoryUrl1 + "firefox/70.0.1/firefox-70.0.1-linux-x64.tgz"; + OperatingSystem os1 = OperatingSystem.LINUX; + SystemArchitecture arch1 = SystemArchitecture.X64; + + CustomToolJson customToolJson = new CustomToolJson(name, version, true, true, repositoryUrl); + CustomToolJson customToolJsonWithOs = new CustomToolJson(name1, version1, false, false, repositoryUrl1); + List customToolJsonList = new ArrayList<>(); + customToolJsonList.add(customToolJson); + customToolJsonList.add(customToolJsonWithOs); + CustomToolsJson customToolsJson = new CustomToolsJson(repositoryUrl, customToolJsonList); + // act + List customToolMetadata = CustomToolsJsonMapper.convert(customToolsJson, context); + // assert + assertThat(customToolMetadata.get(0).getTool()).isEqualTo(name); + assertThat(customToolMetadata.get(0).getVersion()).isEqualTo(VersionIdentifier.of(version)); + assertThat(customToolMetadata.get(0).getOs()).isEqualTo(os); + assertThat(customToolMetadata.get(0).getArch()).isEqualTo(arch); + assertThat(customToolMetadata.get(0).getUrl()).isEqualTo(url); + assertThat(customToolMetadata.get(0).getChecksum()).isNull(); + assertThat(customToolMetadata.get(0).getRepositoryUrl()).isEqualTo(repositoryUrl); + + assertThat(customToolMetadata.get(1).getTool()).isEqualTo(name1); + assertThat(customToolMetadata.get(1).getVersion()).isEqualTo(VersionIdentifier.of(version1)); + assertThat(customToolMetadata.get(1).getOs()).isEqualTo(os1); + assertThat(customToolMetadata.get(1).getArch()).isEqualTo(arch1); + // assert that url was properly created + assertThat(customToolMetadata.get(1).getUrl()).isEqualTo(checkOsArchUrl); + assertThat(customToolMetadata.get(1).getChecksum()).isNull(); + assertThat(customToolMetadata.get(1).getRepositoryUrl()).isEqualTo(repositoryUrl1); + } +} diff --git a/cli/src/test/resources/customtools/ide-custom-tools.json b/cli/src/test/resources/customtools/ide-custom-tools.json new file mode 100644 index 000000000..b2ad041df --- /dev/null +++ b/cli/src/test/resources/customtools/ide-custom-tools.json @@ -0,0 +1,18 @@ +{ + "url": "https://some-file-server.company.com/projects/my-project", + "tools": [ + { + "name": "jboss-eap", + "version": "7.1.4.GA", + "os-agnostic": true, + "arch-agnostic": true + }, + { + "name": "firefox", + "version": "70.0.1", + "os-agnostic": false, + "arch-agnostic": false, + "url": "https://some-file-server.company.com/projects/my-project2" + } + ] +} diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/conf/devon.properties b/cli/src/test/resources/ide-projects/upgrade-settings/project/conf/devon.properties new file mode 100644 index 000000000..ad6cc14a6 --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/conf/devon.properties @@ -0,0 +1,7 @@ +#******************************************************************************** +# This file contains project specific environment variables defined by the user +#******************************************************************************** +DEVON_IDE_HOME=test +DEVON_HOME_DIR=test +DEVON_IDE_CUSTOM_TOOLS=(jboss-eap:7.1.4.GA:all:https://host.tld/projects/my-project firefox:70.0.1:) +JAVA_VERSION=test diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/conf/ide.properties b/cli/src/test/resources/ide-projects/upgrade-settings/project/conf/ide.properties new file mode 100644 index 000000000..1b66dd75c --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/conf/ide.properties @@ -0,0 +1,5 @@ +#******************************************************************************** +# This file contains project specific environment variables defined by the user +#******************************************************************************** + +MVN_VERSION=test diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/home/devon.properties b/cli/src/test/resources/ide-projects/upgrade-settings/project/home/devon.properties new file mode 100644 index 000000000..c532faa1a --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/home/devon.properties @@ -0,0 +1,13 @@ +#******************************************************************************** +# This file contains the global configuration from the user HOME directory. +#******************************************************************************** +DOCKER_EDITION=docker +FOO=foo-${BAR} +TEST_ARGS1=${TEST_ARGS1} user1 +TEST_ARGS2=${TEST_ARGS2} user2 +TEST_ARGS3=${TEST_ARGS3} user3 +TEST_ARGS7=user7 +TEST_ARGS10=user10 +TEST_ARGSb=userb +TEST_ARGSc=${TEST_ARGS1} userc +TEST_ARGSd=${TEST_ARGS1} userd diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/devon.properties b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/devon.properties new file mode 100644 index 000000000..db57079d1 --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/devon.properties @@ -0,0 +1,9 @@ +#******************************************************************************** +# This file contains project specific environment variables +#******************************************************************************** +JAVA_VERSION=17* +MVN_VERSION=3.9.0 +ECLIPSE_VERSION=2023-03 +INTELLIJ_EDITION_TYPE=U +DEVON_IDE_TOOLS=(mvn eclipse) +DEVON_IDE_CUSTOM_TOOLS=(jboss-eap:7.1.4.GA:all:https://host.tld/projects/my-project firefox:70.0.1:) diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/devon/conf/.m2/settings.xml b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/devon/conf/.m2/settings.xml new file mode 100644 index 000000000..8f10a5555 --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/devon/conf/.m2/settings.xml @@ -0,0 +1,87 @@ + + + + ${env.M2_REPO} + + + + + repository + ${env.USERNAME} + $[mavenRepoPassword] + + + + + + + + devonfw-ide + + true + + + + + + + + devonfw-snapshots + + + false + + + + devonfw-snapshots + devonfw SNAPSHOT releases + https://s01.oss.sonatype.org/content/repositories/snapshots/ + + false + never + fail + + + true + never + fail + + + + + + + + + diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/devon/conf/devon.properties b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/devon/conf/devon.properties new file mode 100644 index 000000000..84fbccc23 --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/devon/conf/devon.properties @@ -0,0 +1,4 @@ +#******************************************************************************** +# This file contains project specific environment variables defined by the user +#******************************************************************************** +M2_REPO=~/.m2/repository diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/intellij/workspace/TestXml.xml b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/intellij/workspace/TestXml.xml new file mode 100644 index 000000000..7e5ff4176 --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/intellij/workspace/TestXml.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/projects/IDEasy.properties b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/projects/IDEasy.properties new file mode 100644 index 000000000..ca321ca01 --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/projects/IDEasy.properties @@ -0,0 +1,5 @@ +path=IDEasy +git_url=https://github.com/devonfw/IDEasy.git +git_branch=main +eclipse=import +active=false diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/workspace/testVariableSyntax.txt b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/workspace/testVariableSyntax.txt new file mode 100644 index 000000000..1b733097a --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/settings/workspace/testVariableSyntax.txt @@ -0,0 +1,6 @@ +${DEVON_IDE_HOME} +This is a test text,this is a test text,this is a test text,this is a test text, +this is a test text, +this is a test text,${MAVEN_VERSION}this is a test text,this is a test text,${SETTINGS_PATH} +maven_settings=${DEVON_IDE_HOME}\conf\.m2\settings.xml +this is a test text,this is a test text,this is a test text,this is a test text, diff --git a/cli/src/test/resources/ide-projects/upgrade-settings/project/workspaces/main/devon.properties b/cli/src/test/resources/ide-projects/upgrade-settings/project/workspaces/main/devon.properties new file mode 100644 index 000000000..8fc454ddb --- /dev/null +++ b/cli/src/test/resources/ide-projects/upgrade-settings/project/workspaces/main/devon.properties @@ -0,0 +1,18 @@ +#******************************************************************************** +# This file contains project specific environment variables +#******************************************************************************** +JAVA_VERSION=17* +MVN_VERSION=3.9.0 +ECLIPSE_VERSION=2023-03 +INTELLIJ_EDITION=ultimate +IDE_TOOLS=mvn,eclipse +BAR=bar-${SOME} +TEST_ARGS1=${TEST_ARGS1} settings1 +TEST_ARGS4=${TEST_ARGS4} settings4 +TEST_ARGS5=${TEST_ARGS5} settings5 +TEST_ARGS6=${TEST_ARGS6} settings6 +TEST_ARGS7=${TEST_ARGS7} settings7 +TEST_ARGS8=settings8 +TEST_ARGS9=settings9 +TEST_ARGSb=${TEST_ARGS10} settingsb ${TEST_ARGSa} ${TEST_ARGSb} +TEST_ARGSc=${TEST_ARGSc} settingsc diff --git a/documentation/migration-from-devonfw.adoc b/documentation/migration-from-devonfw.adoc new file mode 100644 index 000000000..450ec8c01 --- /dev/null +++ b/documentation/migration-from-devonfw.adoc @@ -0,0 +1,16 @@ += Migration to IDEasy + +If you used devonfw-ide for your projects so far, and you want to switch to IDEasy, you should follow these steps in order to do it properly: + +Step 1: Follow the https://github.com/devonfw/IDEasy/blob/main/documentation/setup.adoc[setup] guide to properly install IDEasy + +Step 2: Get rid of all legacy by calling the following command: + +[source] +---- +upgrade-settings +---- + +Step 3: After running the command, you might need to update your xml files for our merger. +Please consider reading our documentation for that topic: +https://github.com/devonfw/IDEasy/blob/main/documentation/configurator.adoc diff --git a/documentation/settings.adoc b/documentation/settings.adoc index 721404568..9b5e59bb5 100644 --- a/documentation/settings.adoc +++ b/documentation/settings.adoc @@ -9,9 +9,18 @@ To get an initial set of these settings we provide the default https://github.co These are also released so you can download the latest stable or any history version at http://search.maven.org/#search|ga|1|a%3A%22devonfw-ide-settings%22[maven central]. To test `IDEasy` or for very small projects you can also use these the latest default settings (just hit return when link:setup.adoc[setup] is asking for the `Settings URL`). -However, for collaborative projects we strongly encourage you to distribute and maintain the settings via a dedicated and project specific `git` repository. +However, for collaborative projects we provide two approaches to distribute and maintain the settings: + +* Via a dedicated and project specific git repository (recommended approach). This gives you the freedom to control and manage the tools with their versions and configurations during the project lifecycle. Therefore simply follow the link:usage.adoc#admin[admin usage guide]. +* Via your code repository by including the settings folder directly in your project. +This allows you to keep settings changes in sync with code changes and manage them in the same pull requests. +To use this approach: +** Create a settings folder in your repository root following the structure described below +** Use `ide create --code ` to create your project. +IDEasy will clone your repository and create a symlink to the settings folder. +Changes to settings can then be committed alongside code changes. == Structure