diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliAbortException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliAbortException.java index 175b51da7..99bb832e3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliAbortException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliAbortException.java @@ -1,16 +1,18 @@ -package com.devonfw.tools.ide.cli; - -/** - * {@link CliException} that is thrown if the user aborted further processing due - */ -public final class CliAbortException extends CliException { - - /** - * The constructor. - */ - public CliAbortException() { - - super("Aborted by end-user.", 22); - } - -} +package com.devonfw.tools.ide.cli; + +import com.devonfw.tools.ide.process.ProcessResult; + +/** + * {@link CliException} that is thrown if the user aborted further processing due + */ +public final class CliAbortException extends CliException { + + /** + * The constructor. + */ + public CliAbortException() { + + super("Aborted by end-user.", ProcessResult.ABORT); + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java index 1711fd78e..9d94ec3bc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java @@ -1,5 +1,7 @@ package com.devonfw.tools.ide.cli; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -256,6 +258,20 @@ private CliArgument createStart() { return new CliArgument(NAME_START, this); } + /** + * @return a {@link String} array with all arguments starting from this one. + */ + public String[] asArray() { + + List args = new ArrayList<>(); + CliArgument current = this; + while (!current.isEnd()) { + args.add(current.arg); + current = current.next; + } + return args.toArray(size -> new String[size]); + } + @Override public String toString() { diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliOfflineException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliOfflineException.java new file mode 100644 index 000000000..1395550ee --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliOfflineException.java @@ -0,0 +1,28 @@ +package com.devonfw.tools.ide.cli; + +import com.devonfw.tools.ide.process.ProcessResult; + +/** + * {@link CliException} that is thrown if further processing requires network but the user if offline. + */ +public final class CliOfflineException extends CliException { + + /** + * The constructor. + */ + public CliOfflineException() { + + super("You are offline but network connection is required to perform the operation.", ProcessResult.OFFLINE); + } + + /** + * The constructor. + * + * @param message the {@link #getMessage() message}. + */ + public CliOfflineException(String message) { + + super(message, ProcessResult.OFFLINE); + } + +} 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 4843d621f..ae24b18df 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 @@ -1,21 +1,26 @@ package com.devonfw.tools.ide.commandlet; -import com.devonfw.tools.ide.common.StepContainer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.devonfw.tools.ide.context.GitContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.property.StringProperty; import com.devonfw.tools.ide.repo.CustomTool; +import com.devonfw.tools.ide.step.Step; import com.devonfw.tools.ide.tool.CustomToolCommandlet; import com.devonfw.tools.ide.tool.ToolCommandlet; import com.devonfw.tools.ide.variable.IdeVariables; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - +/** + * Abstract {@link Commandlet} base-class for both {@link UpdateCommandlet} and {@link CreateCommandlet}. + */ public abstract class AbstractUpdateCommandlet extends Commandlet { + /** {@link StringProperty} for the settings repository URL. */ protected final StringProperty settingsRepo; /** @@ -26,7 +31,7 @@ public abstract class AbstractUpdateCommandlet extends Commandlet { public AbstractUpdateCommandlet(IdeContext context) { super(context); - settingsRepo = new StringProperty("", false, "settingsRepository"); + this.settingsRepo = new StringProperty("", false, "settingsRepository"); } @Override @@ -45,7 +50,13 @@ public void run() { } } - setupConf(templatesFolder, this.context.getIdeHome()); + Step step = this.context.newStep("Copy configuration templates", templatesFolder); + try { + setupConf(templatesFolder, this.context.getIdeHome()); + step.success(); + } finally { + step.end(); + } updateSoftware(); } @@ -77,72 +88,77 @@ private void setupConf(Path template, Path conf) { private void updateSettings() { - this.context.info("Updating settings repository ..."); Path settingsPath = this.context.getSettingsPath(); - if (Files.isDirectory(settingsPath) && !this.context.getFileAccess().isEmptyDir(settingsPath)) { - // perform git pull on the settings repo - this.context.getGitContext().pull(settingsPath); - this.context.success("Successfully updated settings repository."); - } else { - // check if a settings repository is given then clone, otherwise prompt user for a repository. - String repository = settingsRepo.getValue(); - if (repository == null) { - String message = "Missing your settings at " + settingsPath + " and no SETTINGS_URL is defined.\n" + - "Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.asciidoc\n" + - "Please contact the technical lead of your project to get the SETTINGS_URL for your project.\n" + - "In case you just want to test IDEasy you may simply hit return to install the default settings.\n" + - "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; + GitContext gitContext = this.context.getGitContext(); + Step step = null; + try { + // here we do not use pullOrClone to prevent asking a pointless question for repository URL... + if (Files.isDirectory(settingsPath) && !this.context.getFileAccess().isEmptyDir(settingsPath)) { + step = this.context.newStep("Pull settings repository"); + gitContext.pull(settingsPath); + } else { + step = this.context.newStep("Clone settings repository"); + // check if a settings repository is given, otherwise prompt user for a repository. + String repository = this.settingsRepo.getValue(); + if (repository == null) { + String message = "Missing your settings at " + settingsPath + " and no SETTINGS_URL is defined.\n" + + "Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.asciidoc\n" + + "Please contact the technical lead of your project to get the SETTINGS_URL for your project.\n" + + "In case you just want to test IDEasy you may simply hit return to install the default settings.\n" + + "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; repository = this.context.askForInput(message, IdeContext.DEFAULT_SETTINGS_REPO_URL); + } else if ("-".equals(repository)) { + repository = IdeContext.DEFAULT_SETTINGS_REPO_URL; + } + gitContext.pullOrClone(repository, settingsPath); + } + step.success("Successfully updated settings repository."); + } finally { + if (step != null) { + step.end(); } - this.context.getGitContext().pullOrClone(repository, settingsPath); - this.context.success("Successfully cloned settings repository."); } } private void updateSoftware() { - Set toolCommandlets = new HashSet<>(); - - // installed tools in IDE_HOME/software - List softwares = this.context.getFileAccess().listChildren(this.context.getSoftwarePath(), Files::isDirectory); - for (Path software : softwares) { - String toolName = software.getFileName().toString(); - ToolCommandlet toolCommandlet = this.context.getCommandletManager().getToolCommandletOrNull(toolName); - if (toolCommandlet != null) { - toolCommandlets.add(toolCommandlet); + Step step = this.context.newStep("Install or update software"); + try { + Set toolCommandlets = new HashSet<>(); + + // installed tools in IDE_HOME/software + List softwares = this.context.getFileAccess().listChildren(this.context.getSoftwarePath(), + Files::isDirectory); + for (Path software : softwares) { + String toolName = software.getFileName().toString(); + ToolCommandlet toolCommandlet = this.context.getCommandletManager().getToolCommandletOrNull(toolName); + if (toolCommandlet != null) { + toolCommandlets.add(toolCommandlet); + } } - } - // regular tools in $IDE_TOOLS - List regularTools = IdeVariables.IDE_TOOLS.get(this.context); - if (regularTools != null) { - for (String regularTool : regularTools) { - toolCommandlets.add(this.context.getCommandletManager().getToolCommandlet(regularTool)); + // regular tools in $IDE_TOOLS + List regularTools = IdeVariables.IDE_TOOLS.get(this.context); + if (regularTools != null) { + for (String regularTool : regularTools) { + toolCommandlets.add(this.context.getCommandletManager().getToolCommandlet(regularTool)); + } } - } - // custom tools in ide-custom-tools.json - for (CustomTool customTool : this.context.getCustomToolRepository().getTools()) { - CustomToolCommandlet customToolCommandlet = new CustomToolCommandlet(this.context, customTool); - toolCommandlets.add(customToolCommandlet); - } + // custom tools in ide-custom-tools.json + for (CustomTool customTool : this.context.getCustomToolRepository().getTools()) { + CustomToolCommandlet customToolCommandlet = new CustomToolCommandlet(this.context, customTool); + toolCommandlets.add(customToolCommandlet); + } - // update/install the toolCommandlets - StepContainer container = new StepContainer(this.context); - for (ToolCommandlet toolCommandlet : toolCommandlets) { - try { - container.startStep(toolCommandlet.getName()); + // update/install the toolCommandlets + for (ToolCommandlet toolCommandlet : toolCommandlets) { toolCommandlet.install(false); - container.endStep(toolCommandlet.getName(), true, null); - } catch (Exception e) { - container.endStep(toolCommandlet.getName(), false, e); } - } - // summary - if (!toolCommandlets.isEmpty()) { - container.complete(); + step.success(); + } finally { + step.end(); } } } - diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java index 9e18715d6..fea5c1b11 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java @@ -1,218 +1,227 @@ -package com.devonfw.tools.ide.commandlet; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import com.devonfw.tools.ide.context.IdeContext; -import com.devonfw.tools.ide.property.KeywordProperty; -import com.devonfw.tools.ide.property.Property; -import com.devonfw.tools.ide.tool.ToolCommandlet; -import com.devonfw.tools.ide.version.VersionIdentifier; - -/** - * A {@link Commandlet} is a sub-command of the IDE CLI. - */ -public abstract class Commandlet { - - /** The {@link IdeContext} instance. */ - protected final IdeContext context; - - private final List> propertiesList; - - private final List> properties; - - private final List> valuesList; - - private final List> values; - - private final Map> optionMap; - - private Property multiValued; - - private String firstKeyword; - - /** - * The constructor. - * - * @param context the {@link IdeContext}. - */ - public Commandlet(IdeContext context) { - - super(); - this.context = context; - this.propertiesList = new ArrayList<>(); - this.properties = Collections.unmodifiableList(this.propertiesList); - this.valuesList = new ArrayList<>(); - this.values = Collections.unmodifiableList(this.valuesList); - this.optionMap = new HashMap<>(); - } - - /** - * @return the {@link List} with all {@link Property properties} of this {@link Commandlet}. - */ - public List> getProperties() { - - return this.properties; - } - - /** - * @return the {@link List} of {@link Property properties} that are {@link Property#isValue() values}. - */ - public List> getValues() { - - return this.values; - } - - /** - * @param nameOrAlias the potential {@link Property#getName() name} or {@link Property#getAlias() alias} of the - * requested {@link Property}. - * @return the requested {@link Property property} or {@code null} if not found. - */ - public Property getOption(String nameOrAlias) { - - return this.optionMap.get(nameOrAlias); - } - - /** - * @param keyword the {@link KeywordProperty keyword} to {@link #add(Property) add}. - */ - protected void addKeyword(String keyword) { - - if (this.properties.isEmpty()) { - this.firstKeyword = keyword; - } - add(new KeywordProperty(keyword, true, null)); - } - - /** - * @param property the keyword {@link Property} to {@link #add(Property) add}. - */ - protected void addKeyword(Property property) { - - if (!this.properties.isEmpty()) { - throw new IllegalStateException(); - } - this.firstKeyword = property.getNameOrAlias(); - add(property); - } - - /** - * @param

type of the {@link Property}. - * @param property the {@link Property} to register. - * @return the given {@link Property}. - */ - protected

> P add(P property) { - - if (this.multiValued != null) { - throw new IllegalStateException( - "The multi-valued property " + this.multiValued + " can not be followed by " + property); - } - this.propertiesList.add(property); - if (property.isOption()) { - add(property.getName(), property, false); - add(property.getAlias(), property, true); - } - if (property.isValue()) { - this.valuesList.add(property); - } - if (property.isMultiValued()) { - this.multiValued = property; - } - return property; - } - - private void add(String name, Property property, boolean alias) { - - if (alias && (name == null)) { - return; - } - Objects.requireNonNull(name); - assert (name.equals(name.trim())); - if (name.isEmpty() && !alias) { - return; - } - Property duplicate = this.optionMap.put(name, property); - if (duplicate != null) { - throw new IllegalStateException("Duplicate name or alias " + name + " for " + property + " and " + duplicate); - } - } - - /** - * @return the name of this {@link Commandlet} (e.g. "help"). - */ - public abstract String getName(); - - /** - * @return the first keyword of this {@link Commandlet}. Typically the same as {@link #getName() name} but may also - * differ (e.g. "set" vs. "set-version"). - */ - public String getKeyword() { - - return this.firstKeyword; - } - - /** - * @param type of the {@link Commandlet}. - * @param commandletType the {@link Class} reflecting the requested {@link Commandlet}. - * @return the requested {@link Commandlet}. - * @see CommandletManager#getCommandlet(Class) - */ - protected C getCommandlet(Class commandletType) { - - return this.context.getCommandletManager().getCommandlet(commandletType); - } - - /** - * @return {@code true} if {@link IdeContext#getIdeHome() IDE_HOME} is required for this commandlet, {@code false} - * otherwise. - */ - public boolean isIdeHomeRequired() { - - return true; - } - - /** - * Runs this {@link Commandlet}. - */ - public abstract void run(); - - /** - * @return {@code true} if this {@link Commandlet} is the valid candidate to be {@link #run()}, {@code false} - * otherwise. - * @see Property#validate() - */ - public boolean validate() { - - // avoid validation exception if not a candidate to be run. - for (Property property : this.propertiesList) { - if (property.isRequired() && (property.getValue() == null)) { - return false; - } - } - for (Property property : this.propertiesList) { - if (!property.validate()) { - return false; - } - } - return true; - } - - @Override - public String toString() { - - return getClass().getSimpleName() + "[" + getName() + "]"; - } - - /** - * @return the {@link ToolCommandlet} set in a {@link Property} of this commandlet used for auto-completion of a - * {@link VersionIdentifier} or {@code null} if not exists or not configured. - */ - public ToolCommandlet getToolForVersionCompletion() { - - return null; - } -} +package com.devonfw.tools.ide.commandlet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.property.KeywordProperty; +import com.devonfw.tools.ide.property.Property; +import com.devonfw.tools.ide.tool.ToolCommandlet; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * A {@link Commandlet} is a sub-command of the IDE CLI. + */ +public abstract class Commandlet { + + /** The {@link IdeContext} instance. */ + protected final IdeContext context; + + private final List> propertiesList; + + private final List> properties; + + private final List> valuesList; + + private final List> values; + + private final Map> optionMap; + + private Property multiValued; + + private String firstKeyword; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public Commandlet(IdeContext context) { + + super(); + this.context = context; + this.propertiesList = new ArrayList<>(); + this.properties = Collections.unmodifiableList(this.propertiesList); + this.valuesList = new ArrayList<>(); + this.values = Collections.unmodifiableList(this.valuesList); + this.optionMap = new HashMap<>(); + } + + /** + * @return the {@link List} with all {@link Property properties} of this {@link Commandlet}. + */ + public List> getProperties() { + + return this.properties; + } + + /** + * @return the {@link List} of {@link Property properties} that are {@link Property#isValue() values}. + */ + public List> getValues() { + + return this.values; + } + + /** + * @param nameOrAlias the potential {@link Property#getName() name} or {@link Property#getAlias() alias} of the + * requested {@link Property}. + * @return the requested {@link Property property} or {@code null} if not found. + */ + public Property getOption(String nameOrAlias) { + + return this.optionMap.get(nameOrAlias); + } + + /** + * @param keyword the {@link KeywordProperty keyword} to {@link #add(Property) add}. + */ + protected void addKeyword(String keyword) { + + if (this.properties.isEmpty()) { + this.firstKeyword = keyword; + } + add(new KeywordProperty(keyword, true, null)); + } + + /** + * @param property the keyword {@link Property} to {@link #add(Property) add}. + */ + protected void addKeyword(Property property) { + + if (!this.properties.isEmpty()) { + throw new IllegalStateException(); + } + this.firstKeyword = property.getNameOrAlias(); + add(property); + } + + /** + * @param

type of the {@link Property}. + * @param property the {@link Property} to register. + * @return the given {@link Property}. + */ + protected

> P add(P property) { + + if (this.multiValued != null) { + throw new IllegalStateException( + "The multi-valued property " + this.multiValued + " can not be followed by " + property); + } + this.propertiesList.add(property); + if (property.isOption()) { + add(property.getName(), property, false); + add(property.getAlias(), property, true); + } + if (property.isValue()) { + this.valuesList.add(property); + } + if (property.isMultiValued()) { + this.multiValued = property; + } + return property; + } + + private void add(String name, Property property, boolean alias) { + + if (alias && (name == null)) { + return; + } + Objects.requireNonNull(name); + assert (name.equals(name.trim())); + if (name.isEmpty() && !alias) { + return; + } + Property duplicate = this.optionMap.put(name, property); + if (duplicate != null) { + throw new IllegalStateException("Duplicate name or alias " + name + " for " + property + " and " + duplicate); + } + } + + /** + * @return the name of this {@link Commandlet} (e.g. "help"). + */ + public abstract String getName(); + + /** + * @return the first keyword of this {@link Commandlet}. Typically the same as {@link #getName() name} but may also + * differ (e.g. "set" vs. "set-version"). + */ + public String getKeyword() { + + return this.firstKeyword; + } + + /** + * @param type of the {@link Commandlet}. + * @param commandletType the {@link Class} reflecting the requested {@link Commandlet}. + * @return the requested {@link Commandlet}. + * @see CommandletManager#getCommandlet(Class) + */ + protected C getCommandlet(Class commandletType) { + + return this.context.getCommandletManager().getCommandlet(commandletType); + } + + /** + * @return {@code true} if {@link IdeContext#getIdeHome() IDE_HOME} is required for this commandlet, {@code false} + * otherwise. + */ + public boolean isIdeHomeRequired() { + + return true; + } + + /** + * @return {@code true} to suppress the {@link com.devonfw.tools.ide.step.StepImpl#logSummary(boolean) step summary + * success message}. + */ + public boolean isSuppressStepSuccess() { + + return false; + } + + /** + * Runs this {@link Commandlet}. + */ + public abstract void run(); + + /** + * @return {@code true} if this {@link Commandlet} is the valid candidate to be {@link #run()}, {@code false} + * otherwise. + * @see Property#validate() + */ + public boolean validate() { + + // avoid validation exception if not a candidate to be run. + for (Property property : this.propertiesList) { + if (property.isRequired() && (property.getValue() == null)) { + return false; + } + } + for (Property property : this.propertiesList) { + if (!property.validate()) { + return false; + } + } + return true; + } + + @Override + public String toString() { + + return getClass().getSimpleName() + "[" + getName() + "]"; + } + + /** + * @return the {@link ToolCommandlet} set in a {@link Property} of this commandlet used for auto-completion of a + * {@link VersionIdentifier} or {@code null} if not exists or not configured. + */ + public ToolCommandlet getToolForVersionCompletion() { + + return null; + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandlet.java index b1bf15c2a..c0dcd68b3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandlet.java @@ -1,13 +1,13 @@ package com.devonfw.tools.ide.commandlet; +import java.util.Collection; + import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.VariableLine; import com.devonfw.tools.ide.os.WindowsPathSyntax; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.variable.IdeVariables; -import java.util.Collection; - /** * {@link Commandlet} to print the environment variables. */ @@ -40,6 +40,12 @@ public boolean isIdeHomeRequired() { return true; } + @Override + public boolean isSuppressStepSuccess() { + + return true; + } + @Override public void run() { diff --git a/cli/src/main/java/com/devonfw/tools/ide/common/StepContainer.java b/cli/src/main/java/com/devonfw/tools/ide/common/StepContainer.java deleted file mode 100644 index bd11e1e91..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/common/StepContainer.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.devonfw.tools.ide.common; - -import com.devonfw.tools.ide.cli.CliException; -import com.devonfw.tools.ide.context.IdeContext; - -import java.util.ArrayList; -import java.util.List; - -/** - * A utility class to manage and log the progress of steps in a process. - * Each step can be started, ended with success or failure, and the overall completion - * status can be checked. - * @throws CliException if one or more steps fail. - */ -public class StepContainer { - - private final IdeContext context; - - /** List of steps that ended successfully. */ - private List successfulSteps; - - /** List of steps that failed. */ - private List failedSteps; - - /** - * The constructor. - * - * @param context the {@link IdeContext}. - */ - public StepContainer(IdeContext context) { - - this.context = context; - successfulSteps = new ArrayList<>(); - failedSteps = new ArrayList<>(); - } - - /** - * Logs the start of a step. - * - * @param stepName the name of the step. - */ - public void startStep(String stepName) { - - this.context.step("Starting step: {}", stepName); - } - - /** - * Logs the end of a step, indicating success or failure. - * - * @param stepName the name of the step. - * @param success {@code true} if the step succeeded, {@code false} otherwise. - * @param e the exception associated with the failure, or {@code null} if the step succeeded. - */ - public void endStep(String stepName, boolean success, Throwable e) { - - if (success) { - successfulSteps.add(stepName); - this.context.success("Step '{}' succeeded.", stepName); - } else { - failedSteps.add(stepName); - this.context.warning("Step '{}' failed.", stepName); - if (e != null) { - this.context.error(e); - } - } - } - - /** - * Checks the overall completion status of all steps. - * - * @throws CliException if one or more steps fail, providing a detailed summary. - */ - public void complete() { - - if (failedSteps.isEmpty()) { - this.context.success("All {} steps ended successfully!", successfulSteps.size()); - } else { - throw new CliException(String.format("%d step(s) failed (%d%%) and %d step(s) succeeded (%d%%) out of %d step(s)!", - failedSteps.size(), calculatePercentage(failedSteps.size()), successfulSteps.size(), - 100 - calculatePercentage(failedSteps.size()), successfulSteps.size() + failedSteps.size())); - } - } - - private int calculatePercentage(int count) { - - return (count * 100) / (successfulSteps.size() + failedSteps.size()); - } - -} 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 b3d1f4dc8..f6417d28c 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 @@ -1,5 +1,17 @@ package com.devonfw.tools.ide.context; +import java.io.IOException; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.cli.CliArgument; import com.devonfw.tools.ide.cli.CliArguments; @@ -32,20 +44,10 @@ import com.devonfw.tools.ide.repo.CustomToolRepositoryImpl; import com.devonfw.tools.ide.repo.DefaultToolRepository; import com.devonfw.tools.ide.repo.ToolRepository; +import com.devonfw.tools.ide.step.Step; +import com.devonfw.tools.ide.step.StepImpl; import com.devonfw.tools.ide.url.model.UrlMetadata; -import java.io.IOException; -import java.net.InetAddress; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; - /** * Abstract base implementation of {@link IdeContext}. */ @@ -121,6 +123,8 @@ public abstract class AbstractIdeContext implements IdeContext { private Path defaultExecutionDirectory; + private StepImpl currentStep; + /** * The constructor. * @@ -128,7 +132,7 @@ public abstract class AbstractIdeContext implements IdeContext { * @param factory the {@link Function} to create {@link IdeSubLogger} per {@link IdeLogLevel}. * @param userDir the optional {@link Path} to current working directory. * @param toolRepository @param toolRepository the {@link ToolRepository} of the context. If it is set to {@code null} - * {@link DefaultToolRepository} will be used. + * {@link DefaultToolRepository} will be used. */ public AbstractIdeContext(IdeLogLevel minLogLevel, Function factory, Path userDir, ToolRepository toolRepository) { @@ -166,17 +170,12 @@ public AbstractIdeContext(IdeLogLevel minLogLevel, Function complete(CliArguments arguments, boolean includ /** * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are - * matched. Consider passing a {@link CliArguments#copy() copy} as needed. + * matched. Consider passing a {@link CliArguments#copy() copy} as needed. * @param cmd the potential {@link Commandlet} to match. * @param collector the {@link CompletionCandidateCollector}. * @return {@code true} if the given {@link Commandlet} matches to the given {@link CliArgument}(s) and those have - * been applied (set in the {@link Commandlet} and {@link Commandlet#validate() validated}), {@code false} otherwise - * (the {@link Commandlet} did not match and we have to try a different candidate). + * been applied (set in the {@link Commandlet} and {@link Commandlet#validate() validated}), {@code false} + * otherwise (the {@link Commandlet} did not match and we have to try a different candidate). */ public boolean apply(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/GitContext.java index fa3a21bff..d2e201e38 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/GitContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/GitContext.java @@ -2,21 +2,26 @@ import java.nio.file.Path; -import com.devonfw.tools.ide.log.IdeLogger; +import com.devonfw.tools.ide.cli.CliOfflineException; /** * Interface for git commands with input and output of information for the user. */ -public interface GitContext extends IdeLogger { +public interface GitContext { + + /** The default git remote name. */ + String DEFAULT_REMOTE = "origin"; /** * Checks if the Git repository in the specified target folder needs an update by inspecting the modification time of * a magic file. * * @param repoUrl the git remote URL to clone from. + * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch. * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. - * It is not the parent directory where git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the + * final folder that will contain the ".git" subfolder. + * @throws CliOfflineException if offline and cloning is needed. */ void pullOrCloneIfNeeded(String repoUrl, String branch, Path targetRepository); @@ -24,21 +29,52 @@ public interface GitContext extends IdeLogger { * Attempts a git pull and reset if required. * * @param repoUrl the git remote URL to clone from. - * @param branch the branch name e.g. master. * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. - * It is not the parent directory where git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the + * final folder that will contain the ".git" subfolder. + * @throws CliOfflineException if offline and cloning is needed. + */ + default void pullOrCloneAndResetIfNeeded(String repoUrl, Path targetRepository) { + + pullOrCloneAndResetIfNeeded(repoUrl, targetRepository, null); + } + + /** + * Attempts a git pull and reset if required. + * + * @param repoUrl the git remote URL to clone from. + * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the + * final folder that will contain the ".git" subfolder. + * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch. + * @throws CliOfflineException if offline and cloning is needed. + */ + default void pullOrCloneAndResetIfNeeded(String repoUrl, Path targetRepository, String branch) { + + pullOrCloneAndResetIfNeeded(repoUrl, targetRepository, branch, null); + } + + /** + * Attempts a git pull and reset if required. + * + * @param repoUrl the git remote URL to clone from. + * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the + * final folder that will contain the ".git" subfolder. + * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch. * @param remoteName the remote name e.g. origin. + * @throws CliOfflineException if offline and cloning is needed. */ - void pullOrFetchAndResetIfNeeded(String repoUrl, String branch, Path targetRepository, String remoteName); + void pullOrCloneAndResetIfNeeded(String repoUrl, Path targetRepository, String branch, String remoteName); /** * Runs a git pull or a git clone. * * @param gitRepoUrl the git remote URL to clone from. * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. - * It is not the parent directory where git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the + * final folder that will contain the ".git" subfolder. + * @throws CliOfflineException if offline and cloning is needed. */ void pullOrClone(String gitRepoUrl, Path targetRepository); @@ -46,20 +82,22 @@ public interface GitContext extends IdeLogger { * Runs a git pull or a git clone. * * @param gitRepoUrl the git remote URL to clone from. - * @param branch the branch name e.g. master. + * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch. * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. - * It is not the parent directory where git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the + * final folder that will contain the ".git" subfolder. + * @throws CliOfflineException if offline and cloning is needed. */ void pullOrClone(String gitRepoUrl, String branch, Path targetRepository); /** - * Runs a git clone. Throws a CliException if in offline mode. + * Runs a git clone. * * @param gitRepoUrl the {@link GitUrl} to use for the repository URL. * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. - * It is not the parent directory where git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the * + * final folder that will contain the ".git" subfolder. + * @throws CliOfflineException if offline and cloning is needed. */ void clone(GitUrl gitRepoUrl, Path targetRepository); @@ -67,28 +105,53 @@ public interface GitContext extends IdeLogger { * Runs a git pull. * * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. - * It is not the parent directory where git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the * + * final folder that will contain the ".git" subfolder. */ void pull(Path targetRepository); /** - * Runs a git reset if files were modified. + * Runs a git diff-index to detect local changes and if so reverts them via git reset. + * + * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the + * final folder that will contain the ".git" subfolder. + */ + default void reset(Path targetRepository) { + + reset(targetRepository, null); + } + + /** + * Runs a git diff-index to detect local changes and if so reverts them via git reset. + * + * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the + * final folder that will contain the ".git" subfolder. + * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch. + */ + default void reset(Path targetRepository, String branch) { + + reset(targetRepository, branch, null); + } + + /** + * Runs a git reset reverting all local changes to the git repository. * * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. - * It is not the parent directory where git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. - * @param remoteName the remote server name. - * @param branchName the name of the branch. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the * + * final folder that will contain the ".git" subfolder. + * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch. + * @param remoteName the name of the git remote e.g. "origin". */ - void reset(Path targetRepository, String remoteName, String branchName); + void reset(Path targetRepository, String branch, String remoteName); /** * Runs a git cleanup if untracked files were found. * * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. - * It is not the parent directory where git will by default create a sub-folder by default on clone but the * final - * folder that will contain the ".git" subfolder. + * It is not the parent directory where git will by default create a sub-folder by default on clone but the * + * final folder that will contain the ".git" subfolder. */ void cleanup(Path targetRepository); diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java index e7a435500..c42dd92ec 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java @@ -12,9 +12,7 @@ import java.util.Map; import java.util.Objects; -import com.devonfw.tools.ide.cli.CliException; -import com.devonfw.tools.ide.log.IdeLogLevel; -import com.devonfw.tools.ide.log.IdeSubLogger; +import com.devonfw.tools.ide.cli.CliOfflineException; import com.devonfw.tools.ide.process.ProcessContext; import com.devonfw.tools.ide.process.ProcessErrorHandling; import com.devonfw.tools.ide.process.ProcessMode; @@ -24,13 +22,13 @@ * Implements the {@link GitContext}. */ public class GitContextImpl implements GitContext { - private static final Duration GIT_PULL_CACHE_DELAY_MILLIS = Duration.ofMillis(30 * 60 * 1000); + private static final Duration GIT_PULL_CACHE_DELAY_MILLIS = Duration.ofMinutes(30); private final IdeContext context; private final ProcessContext processContext; - private final ProcessMode PROCESS_MODE = ProcessMode.DEFAULT; + private static final ProcessMode PROCESS_MODE = ProcessMode.DEFAULT; /** * @param context the {@link IdeContext context}. @@ -47,41 +45,36 @@ public void pullOrCloneIfNeeded(String repoUrl, String branch, Path targetReposi Path gitDirectory = targetRepository.resolve(".git"); // Check if the .git directory exists - if (Files.isDirectory(gitDirectory)) { + if (!this.context.isForceMode() && Files.isDirectory(gitDirectory)) { Path magicFilePath = gitDirectory.resolve("HEAD"); long currentTime = System.currentTimeMillis(); // Get the modification time of the magic file - long fileMTime; try { - fileMTime = Files.getLastModifiedTime(magicFilePath).toMillis(); - } catch (IOException e) { - throw new IllegalStateException("Could not read " + magicFilePath, e); - } - - // Check if the file modification time is older than the delta threshold - if ((currentTime - fileMTime > GIT_PULL_CACHE_DELAY_MILLIS.toMillis()) || context.isForceMode()) { - pullOrClone(repoUrl, targetRepository); - try { - Files.setLastModifiedTime(magicFilePath, FileTime.fromMillis(currentTime)); - } catch (IOException e) { - throw new IllegalStateException("Could not read or write in " + magicFilePath, e); + long fileModifiedTime = Files.getLastModifiedTime(magicFilePath).toMillis(); + // Check if the file modification time is older than the delta threshold + if ((currentTime - fileModifiedTime > GIT_PULL_CACHE_DELAY_MILLIS.toMillis())) { + pullOrClone(repoUrl, targetRepository); + try { + Files.setLastModifiedTime(magicFilePath, FileTime.fromMillis(currentTime)); + } catch (IOException e) { + this.context.warning().log(e, "Cound not update modification-time of {}", magicFilePath); + } + return; } + } catch (IOException e) { + this.context.error(e); } - } else { - // If the .git directory does not exist, perform git clone - pullOrClone(repoUrl, branch, targetRepository); } + // If the .git directory does not exist or in case of an error, perform git operation directly + pullOrClone(repoUrl, branch, targetRepository); } - public void pullOrFetchAndResetIfNeeded(String repoUrl, String branch, Path targetRepository, String remoteName) { + @Override + public void pullOrCloneAndResetIfNeeded(String repoUrl, Path targetRepository, String branch, String remoteName) { pullOrCloneIfNeeded(repoUrl, branch, targetRepository); - if (remoteName.isEmpty()) { - reset(targetRepository, "origin", "master"); - } else { - reset(targetRepository, remoteName, "master"); - } + reset(targetRepository, "master", remoteName); cleanup(targetRepository); } @@ -104,7 +97,8 @@ public void pullOrClone(String gitRepoUrl, String branch, Path targetRepository) if (Files.isDirectory(targetRepository.resolve(".git"))) { // checks for remotes - ProcessResult result = this.processContext.addArg("remote").run(PROCESS_MODE); + this.processContext.directory(targetRepository); + ProcessResult result = this.processContext.addArg("remote").run(ProcessMode.DEFAULT_CAPTURE); List remotes = result.getOut(); if (remotes.isEmpty()) { String message = targetRepository @@ -179,7 +173,8 @@ public void clone(GitUrl gitRepoUrl, Path targetRepository) { } } } else { - throw new CliException("Could not clone " + parsedUrl + " to " + targetRepository + " because you are offline."); + throw new CliOfflineException( + "Could not clone " + parsedUrl + " to " + targetRepository + " because you are offline."); } } @@ -193,7 +188,7 @@ public void pull(Path targetRepository) { if (!result.isSuccessful()) { Map remoteAndBranchName = retrieveRemoteAndBranchName(); - context.warning("Git pull for {}/{} failed for repository {}.", remoteAndBranchName.get("remote"), + this.context.warning("Git pull for {}/{} failed for repository {}.", remoteAndBranchName.get("remote"), remoteAndBranchName.get("branch"), targetRepository); handleErrors(targetRepository, result); } @@ -228,8 +223,11 @@ private Map retrieveRemoteAndBranchName() { } @Override - public void reset(Path targetRepository, String remoteName, String branchName) { + public void reset(Path targetRepository, String branchName, String remoteName) { + if ((remoteName == null) || remoteName.isEmpty()) { + remoteName = DEFAULT_REMOTE; + } this.processContext.directory(targetRepository); ProcessResult result; // check for changed files @@ -237,13 +235,13 @@ public void reset(Path targetRepository, String remoteName, String branchName) { if (!result.isSuccessful()) { // reset to origin/master - context.warning("Git has detected modified files -- attempting to reset {} to '{}/{}'.", targetRepository, + this.context.warning("Git has detected modified files -- attempting to reset {} to '{}/{}'.", targetRepository, remoteName, branchName); result = this.processContext.addArg("reset").addArg("--hard").addArg(remoteName + "/" + branchName) .run(PROCESS_MODE); if (!result.isSuccessful()) { - context.warning("Git failed to reset {} to '{}/{}'.", remoteName, branchName, targetRepository); + this.context.warning("Git failed to reset {} to '{}/{}'.", remoteName, branchName, targetRepository); handleErrors(targetRepository, result); } } @@ -256,22 +254,17 @@ public void cleanup(Path targetRepository) { ProcessResult result; // check for untracked files result = this.processContext.addArg("ls-files").addArg("--other").addArg("--directory").addArg("--exclude-standard") - .run(PROCESS_MODE); + .run(ProcessMode.DEFAULT_CAPTURE); if (!result.getOut().isEmpty()) { // delete untracked files - context.warning("Git detected untracked files in {} and is attempting a cleanup.", targetRepository); + this.context.warning("Git detected untracked files in {} and is attempting a cleanup.", targetRepository); result = this.processContext.addArg("clean").addArg("-df").run(PROCESS_MODE); if (!result.isSuccessful()) { - context.warning("Git failed to clean the repository {}.", targetRepository); + this.context.warning("Git failed to clean the repository {}.", targetRepository); } } } - @Override - public IdeSubLogger level(IdeLogLevel level) { - - return null; - } } 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 d6ee85ffd..8961ba455 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 @@ -5,6 +5,7 @@ import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.cli.CliOfflineException; import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.common.SystemPath; import com.devonfw.tools.ide.environment.EnvironmentVariables; @@ -17,6 +18,7 @@ import com.devonfw.tools.ide.process.ProcessContext; import com.devonfw.tools.ide.repo.CustomToolRepository; import com.devonfw.tools.ide.repo.ToolRepository; +import com.devonfw.tools.ide.step.Step; import com.devonfw.tools.ide.url.model.UrlMetadata; import com.devonfw.tools.ide.variable.IdeVariables; @@ -25,7 +27,12 @@ */ public interface IdeContext extends IdeLogger { - String DEFAULT_SETTINGS_REPO_URL = "https://github.com/devonfw/ide-settings"; + /** + * The default settings URL. + * + * @see com.devonfw.tools.ide.commandlet.AbstractUpdateCommandlet + */ + String DEFAULT_SETTINGS_REPO_URL = "https://github.com/devonfw/ide-settings.git"; /** The name of the workspaces folder. */ String FOLDER_WORKSPACES = "workspaces"; @@ -120,8 +127,10 @@ public interface IdeContext extends IdeLogger { /** The default for {@link #getWorkspaceName()}. */ String WORKSPACE_MAIN = "main"; + /** The folder with the configuration template files from the settings. */ String FOLDER_TEMPLATES = "templates"; + /** Legacy folder name used as compatibility fallback if {@link #FOLDER_TEMPLATES} does not exist. */ String FOLDER_LEGACY_TEMPLATES = "devon"; /** @@ -214,7 +223,7 @@ default void askToContinue(String question) { default void requireOnline(String purpose) { if (isOfflineMode()) { - throw new CliException("You are offline but Internet access is required for " + purpose, 23); + throw new CliOfflineException("You are offline but Internet access is required for " + purpose); } } @@ -410,9 +419,41 @@ default void requireOnline(String purpose) { GitContext getGitContext(); /** - * Updates the current working directory (CWD) and configures the environment paths according to the specified parameters. - * This method is central to changing the IDE's notion of where it operates, affecting where configurations, workspaces, - * settings, and other resources are located or loaded from. + * @return the current {@link Step} of processing. + */ + Step getCurrentStep(); + + /** + * @param name the {@link Step#getName() name} of the new {@link Step}. + * @return the new {@link Step} that has been created and started. + */ + default Step newStep(String name) { + + return newStep(name, Step.NO_PARAMS); + } + + /** + * @param name the {@link Step#getName() name} of the new {@link Step}. + * @param parameters the {@link Step#getParameter(int) parameters} of the {@link Step}. + * @return the new {@link Step} that has been created and started. + */ + default Step newStep(String name, Object... parameters) { + + return newStep(false, name, parameters); + } + + /** + * @param silent the {@link Step#isSilent() silent flag}. + * @param name the {@link Step#getName() name} of the new {@link Step}. + * @param parameters the {@link Step#getParameter(int) parameters} of the {@link Step}. + * @return the new {@link Step} that has been created and started. + */ + Step newStep(boolean silent, String name, Object... parameters); + + /** + * Updates the current working directory (CWD) and configures the environment paths according to the specified + * parameters. This method is central to changing the IDE's notion of where it operates, affecting where + * configurations, workspaces, settings, and other resources are located or loaded from. * * @param ideHome The path to the IDE home directory. */ @@ -422,9 +463,9 @@ default void setIdeHome(Path ideHome) { } /** - * Updates the current working directory (CWD) and configures the environment paths according to the specified parameters. - * This method is central to changing the IDE's notion of where it operates, affecting where configurations, workspaces, - * settings, and other resources are located or loaded from. + * Updates the current working directory (CWD) and configures the environment paths according to the specified + * parameters. This method is central to changing the IDE's notion of where it operates, affecting where + * configurations, workspaces, settings, and other resources are located or loaded from. * * @param userDir The path to set as the current working directory. * @param workspace The name of the workspace within the IDE's environment. diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java index aa56c36d6..4f114459e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java @@ -26,7 +26,7 @@ public class IdeContextConsole extends AbstractIdeContext { */ public IdeContextConsole(IdeLogLevel minLogLevel, Appendable out, boolean colored) { - super(minLogLevel, level -> new IdeSubLoggerOut(level, out, colored), null, null); + super(minLogLevel, level -> new IdeSubLoggerOut(level, out, colored, minLogLevel), null, null); if (System.console() == null) { debug("System console not available - using System.in as fallback"); this.scanner = new Scanner(System.in); @@ -54,7 +54,7 @@ public IdeProgressBar prepareProgressBar(String taskName, long size) { .rightBracket("│\u001b[0m").block('█').space(' ').fractionSymbols(" ▏▎▍▌▋▊▉").rightSideFractionSymbol(' ') .build()); // set different style for Windows systems (ASCII) - if (this.getSystemInfo().isWindows()) { + if (getSystemInfo().isWindows()) { pbb.setStyle(ProgressBarStyle.builder().refreshPrompt("\r").leftBracket("[").delimitingSequence("") .rightBracket("]").block('=').space(' ').fractionSymbols(">").rightSideFractionSymbol(' ').build()); } 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 99bf6d1fe..d411b2280 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 @@ -208,16 +208,14 @@ private String resolve(String value, Object src, int recursion, Object rootSrc, StringBuilder sb = new StringBuilder(value.length() + EXTRA_CAPACITY); do { String variableName = matcher.group(2); - String variableValue = resolvedVars.getValue(variableName); + String variableValue = resolvedVars.getValue(variableName, false); if (variableValue == null) { this.context.warning("Undefined variable {} in '{}={}' for root '{}={}'", variableName, src, value, rootSrc, rootValue); continue; } EnvironmentVariables lowestFound = findVariable(variableName); - boolean isNotSelfReferencing = lowestFound == null || !lowestFound.getFlat(variableName).equals(value); - - if (isNotSelfReferencing) { + if ((lowestFound == null) || !lowestFound.getFlat(variableName).equals(value)) { // looking for "variableName" starting from resolved upwards the hierarchy String replacement = resolvedVars.resolve(variableValue, variableName, recursion, rootSrc, rootValue, resolvedVars); @@ -254,9 +252,12 @@ private String resolve(String value, Object src, int recursion, Object rootSrc, * default values. * * @param name the name of the variable to get. + * @param ignoreDefaultValue - {@code true} if the {@link VariableDefinition#getDefaultValue(IdeContext) default + * value} of a potential {@link VariableDefinition} shall be ignored, {@code false} to return default instead + * of {@code null}. * @return the value of the variable. */ - protected String getValue(String name) { + protected String getValue(String name, boolean ignoreDefaultValue) { VariableDefinition var = IdeVariables.get(name); String value; @@ -268,9 +269,11 @@ protected String getValue(String name) { if ((value == null) && (var != null)) { String key = var.getName(); if (!name.equals(key)) { + // try new name (e.g. IDE_TOOLS or IDE_HOME) if no value could be found by given legacy name (e.g. + // DEVON_IDE_TOOLS or DEVON_IDE_HOME) value = this.parent.get(key); } - if (value == null) { + if ((value == null) && !ignoreDefaultValue) { value = var.getDefaultValueAsString(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 bf6be144c..49b1e36e5 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 @@ -1,237 +1,250 @@ -package com.devonfw.tools.ide.environment; - -import java.nio.file.Path; -import java.util.Collection; -import java.util.Locale; - -import com.devonfw.tools.ide.context.IdeContext; -import com.devonfw.tools.ide.version.VersionIdentifier; - -/** - * Interface for the environment with the variables. - */ -public interface EnvironmentVariables { - - /** Filename of the default variable configuration file. {@value} */ - String DEFAULT_PROPERTIES = "ide.properties"; - - /** Filename of the legacy variable configuration file. {@value} */ - String LEGACY_PROPERTIES = "devon.properties"; - - /** - * @param name the name of the environment variable to get. - * @return the value of the variable with the given {@code name}. Will be {@code null} if no such variable is defined. - */ - default String get(String name) { - - String value = getFlat(name); - if (value == null) { - EnvironmentVariables parent = getParent(); - if (parent != null) { - value = parent.get(name); - } - } - return value; - } - - /** - * @param name the name of the environment variable to get. - * @return the value of the variable with the given {@code name} as {@link Path}. Will be {@code null} if no such - * variable is defined. - */ - default Path getPath(String name) { - - String value = get(name); - if (value == null) { - return null; - } - return Path.of(value); - } - - /** - * @param name the name of the environment variable to get. - * @return the value of the variable with the given {@code name} without {@link #getParent() inheritance from parent}. - * Will be {@code null} if no such variable is defined. - */ - String getFlat(String name); - - /** - * @param tool the name of the tool (e.g. "java"). - * @return the edition of the tool to use. - */ - default String getToolEdition(String tool) { - - String variable = tool.toUpperCase(Locale.ROOT) + "_EDITION"; - String value = get(variable); - if (value == null) { - value = tool; - } - return value; - } - - /** - * @param tool the name of the tool (e.g. "java"). - * @return the {@link VersionIdentifier} with the version of the tool to use. May also be a - * {@link VersionIdentifier#isPattern() version pattern}. Will be {@link VersionIdentifier#LATEST} if - * undefined. - */ - default VersionIdentifier getToolVersion(String tool) { - - String variable = getToolVersionVariable(tool); - String value = get(variable); - if (value == null) { - return VersionIdentifier.LATEST; - } - return VersionIdentifier.of(value); - } - - /** - * @return the {@link EnvironmentVariablesType type} of this {@link EnvironmentVariables}. - */ - EnvironmentVariablesType getType(); - - /** - * @param type the {@link #getType() type} of the requested {@link EnvironmentVariables}. - * @return the {@link EnvironmentVariables} with the given {@link #getType() type} from this - * {@link EnvironmentVariables} along the {@link #getParent() parent} hierarchy or {@code null} if not found. - */ - default EnvironmentVariables getByType(EnvironmentVariablesType type) { - - if (type == getType()) { - return this; - } - EnvironmentVariables parent = getParent(); - if (parent == null) { - return null; - } else { - return parent.getByType(type); - } - } - - /** - * @return the {@link Path} to the underlying properties file or {@code null} if not based on such file (e.g. for EVS - * or {@link EnvironmentVariablesResolved}). - */ - Path getPropertiesFilePath(); - - /** - * @return the source identifier describing this {@link EnvironmentVariables} for debugging. - */ - String getSource(); - - /** - * @return the parent {@link EnvironmentVariables} to inherit from or {@code null} if this is the - * {@link EnvironmentVariablesType#SYSTEM root} {@link EnvironmentVariables} instance. - */ - 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. - * @param export - {@code true} if the variable needs to be exported, {@code false} otherwise. - * @return the old variable value. - */ - default String set(String name, String value, boolean export) { - - throw new UnsupportedOperationException(); - } - - /** - * Saves any potential {@link #set(String, String, boolean) changes} of this {@link EnvironmentVariables}. - */ - default void save() { - - throw new UnsupportedOperationException("Not yet implemented!"); - } - - /** - * @param name the {@link com.devonfw.tools.ide.variable.VariableDefinition#getName() name} of the variable to search - * for. - * @return the closest {@link EnvironmentVariables} instance that defines the variable with the given {@code name} or - * {@code null} if the variable is not defined. - */ - default EnvironmentVariables findVariable(String name) { - - String value = getFlat(name); - if (value != null) { - return this; - } - EnvironmentVariables parent = getParent(); - if (parent == null) { - return null; - } else { - return parent.findVariable(name); - } - } - - /** - * @return the {@link Collection} of the {@link VariableLine}s defined by this {@link EnvironmentVariables} including - * inheritance. - */ - Collection collectVariables(); - - /** - * @return the {@link Collection} of the {@link VariableLine#isExport() exported} {@link VariableLine}s defined by - * this {@link EnvironmentVariables} including inheritance. - */ - Collection collectExportedVariables(); - - /** - * @param string the {@link String} that potentially contains variables in the syntax "${«variable«}". Those will be - * resolved by this method and replaced with their {@link #get(String) value}. - * @param source the source where the {@link String} to resolve originates from. Should have a reasonable - * {@link Object#toString() string representation} that will be used in error or log messages if a variable - * could not be resolved. - * @return the given {@link String} with the variables resolved. - * @see com.devonfw.tools.ide.tool.ide.IdeToolCommandlet - */ - String resolve(String string, Object source); - - /** - * The inverse operation of {@link #resolve(String, Object)}. Please note that the {@link #resolve(String, Object) - * resolve} operation is not fully bijective. There may be multiple variables holding the same {@link #get(String) - * value} or there may be static text that can be equal to a {@link #get(String) variable value}. This method does its - * best to implement the inverse resolution based on some heuristics. - * - * @param string the {@link String} where to find {@link #get(String) variable values} and replace them with according - * "${«variable«}" expressions. - * @param source the source where the {@link String} to inverse resolve originates from. Should have a reasonable - * {@link Object#toString() string representation} that will be used in error or log messages if the inverse - * resolving was not working as expected. - * @return the given {@link String} with {@link #get(String) variable values} replaced with according "${«variable«}" - * expressions. - * @see com.devonfw.tools.ide.tool.ide.IdeToolCommandlet - */ - String inverseResolve(String string, Object source); - - /** - * @param context the {@link IdeContext}. - * @return the system {@link EnvironmentVariables} building the root of the {@link EnvironmentVariables} hierarchy. - */ - static AbstractEnvironmentVariables ofSystem(IdeContext context) { - - return EnvironmentVariablesSystem.of(context); - } - - /** - * @param tool the name of the tool. - * @return the name of the version variable. - */ - static String getToolVersionVariable(String tool) { - - return tool.toUpperCase(Locale.ROOT) + "_VERSION"; - } - - /** - * @param tool the name of the tool. - * @return the name of the edition variable. - */ - static String getToolEditionVariable(String tool) { - - return tool.toUpperCase(Locale.ROOT) + "_EDITION"; - } - -} +package com.devonfw.tools.ide.environment; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.Locale; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.variable.VariableDefinition; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * Interface for the environment with the variables. + */ +public interface EnvironmentVariables { + + /** Filename of the default variable configuration file. {@value} */ + String DEFAULT_PROPERTIES = "ide.properties"; + + /** Filename of the legacy variable configuration file. {@value} */ + String LEGACY_PROPERTIES = "devon.properties"; + + /** + * @param name the name of the environment variable to get. + * @return the value of the variable with the given {@code name}. Will be {@code null} if no such variable is defined. + */ + default String get(String name) { + + return get(name, false); + } + + /** + * @param name the name of the environment variable to get. + * @param ignoreDefaultValue - {@code true} if the {@link VariableDefinition#getDefaultValue(IdeContext) default + * value} of a potential {@link VariableDefinition} shall be ignored, {@code false} to return default instead + * of {@code null}. + * @return the value of the variable with the given {@code name}. Will be {@code null} if no such variable is defined. + */ + default String get(String name, boolean ignoreDefaultValue) { + + String value = getFlat(name); + if (value == null) { + EnvironmentVariables parent = getParent(); + if (parent != null) { + value = parent.get(name); + } + } + return value; + } + + /** + * @param name the name of the environment variable to get. + * @return the value of the variable with the given {@code name} as {@link Path}. Will be {@code null} if no such + * variable is defined. + */ + default Path getPath(String name) { + + String value = get(name); + if (value == null) { + return null; + } + return Path.of(value); + } + + /** + * @param name the name of the environment variable to get. + * @return the value of the variable with the given {@code name} without {@link #getParent() inheritance from parent}. + * Will be {@code null} if no such variable is defined. + */ + String getFlat(String name); + + /** + * @param tool the name of the tool (e.g. "java"). + * @return the edition of the tool to use. + */ + default String getToolEdition(String tool) { + + String variable = tool.toUpperCase(Locale.ROOT) + "_EDITION"; + String value = get(variable); + if (value == null) { + value = tool; + } + return value; + } + + /** + * @param tool the name of the tool (e.g. "java"). + * @return the {@link VersionIdentifier} with the version of the tool to use. May also be a + * {@link VersionIdentifier#isPattern() version pattern}. Will be {@link VersionIdentifier#LATEST} if + * undefined. + */ + default VersionIdentifier getToolVersion(String tool) { + + String variable = getToolVersionVariable(tool); + String value = get(variable); + if (value == null) { + return VersionIdentifier.LATEST; + } + return VersionIdentifier.of(value); + } + + /** + * @return the {@link EnvironmentVariablesType type} of this {@link EnvironmentVariables}. + */ + EnvironmentVariablesType getType(); + + /** + * @param type the {@link #getType() type} of the requested {@link EnvironmentVariables}. + * @return the {@link EnvironmentVariables} with the given {@link #getType() type} from this + * {@link EnvironmentVariables} along the {@link #getParent() parent} hierarchy or {@code null} if not found. + */ + default EnvironmentVariables getByType(EnvironmentVariablesType type) { + + if (type == getType()) { + return this; + } + EnvironmentVariables parent = getParent(); + if (parent == null) { + return null; + } else { + return parent.getByType(type); + } + } + + /** + * @return the {@link Path} to the underlying properties file or {@code null} if not based on such file (e.g. for EVS + * or {@link EnvironmentVariablesResolved}). + */ + Path getPropertiesFilePath(); + + /** + * @return the source identifier describing this {@link EnvironmentVariables} for debugging. + */ + String getSource(); + + /** + * @return the parent {@link EnvironmentVariables} to inherit from or {@code null} if this is the + * {@link EnvironmentVariablesType#SYSTEM root} {@link EnvironmentVariables} instance. + */ + 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. + * @param export - {@code true} if the variable needs to be exported, {@code false} otherwise. + * @return the old variable value. + */ + default String set(String name, String value, boolean export) { + + throw new UnsupportedOperationException(); + } + + /** + * Saves any potential {@link #set(String, String, boolean) changes} of this {@link EnvironmentVariables}. + */ + default void save() { + + throw new UnsupportedOperationException("Not yet implemented!"); + } + + /** + * @param name the {@link com.devonfw.tools.ide.variable.VariableDefinition#getName() name} of the variable to search + * for. + * @return the closest {@link EnvironmentVariables} instance that defines the variable with the given {@code name} or + * {@code null} if the variable is not defined. + */ + default EnvironmentVariables findVariable(String name) { + + String value = getFlat(name); + if (value != null) { + return this; + } + EnvironmentVariables parent = getParent(); + if (parent == null) { + return null; + } else { + return parent.findVariable(name); + } + } + + /** + * @return the {@link Collection} of the {@link VariableLine}s defined by this {@link EnvironmentVariables} including + * inheritance. + */ + Collection collectVariables(); + + /** + * @return the {@link Collection} of the {@link VariableLine#isExport() exported} {@link VariableLine}s defined by + * this {@link EnvironmentVariables} including inheritance. + */ + Collection collectExportedVariables(); + + /** + * @param string the {@link String} that potentially contains variables in the syntax "${«variable«}". Those will be + * resolved by this method and replaced with their {@link #get(String) value}. + * @param source the source where the {@link String} to resolve originates from. Should have a reasonable + * {@link Object#toString() string representation} that will be used in error or log messages if a variable + * could not be resolved. + * @return the given {@link String} with the variables resolved. + * @see com.devonfw.tools.ide.tool.ide.IdeToolCommandlet + */ + String resolve(String string, Object source); + + /** + * The inverse operation of {@link #resolve(String, Object)}. Please note that the {@link #resolve(String, Object) + * resolve} operation is not fully bijective. There may be multiple variables holding the same {@link #get(String) + * value} or there may be static text that can be equal to a {@link #get(String) variable value}. This method does its + * best to implement the inverse resolution based on some heuristics. + * + * @param string the {@link String} where to find {@link #get(String) variable values} and replace them with according + * "${«variable«}" expressions. + * @param source the source where the {@link String} to inverse resolve originates from. Should have a reasonable + * {@link Object#toString() string representation} that will be used in error or log messages if the inverse + * resolving was not working as expected. + * @return the given {@link String} with {@link #get(String) variable values} replaced with according "${«variable«}" + * expressions. + * @see com.devonfw.tools.ide.tool.ide.IdeToolCommandlet + */ + String inverseResolve(String string, Object source); + + /** + * @param context the {@link IdeContext}. + * @return the system {@link EnvironmentVariables} building the root of the {@link EnvironmentVariables} hierarchy. + */ + static AbstractEnvironmentVariables ofSystem(IdeContext context) { + + return EnvironmentVariablesSystem.of(context); + } + + /** + * @param tool the name of the tool. + * @return the name of the version variable. + */ + static String getToolVersionVariable(String tool) { + + return tool.toUpperCase(Locale.ROOT) + "_VERSION"; + } + + /** + * @param tool the name of the tool. + * @return the name of the edition variable. + */ + static String getToolEditionVariable(String tool) { + + return tool.toUpperCase(Locale.ROOT) + "_EDITION"; + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesResolved.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesResolved.java index 23bb69f4f..634340974 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesResolved.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesResolved.java @@ -1,10 +1,10 @@ package com.devonfw.tools.ide.environment; +import java.util.Set; + import com.devonfw.tools.ide.variable.IdeVariables; import com.devonfw.tools.ide.variable.VariableDefinition; -import java.util.Set; - /** * Implementation of {@link EnvironmentVariables} that resolves variables recursively. */ @@ -33,9 +33,9 @@ public String getFlat(String name) { } @Override - public String get(String name) { + public String get(String name, boolean ignoreDefaultValue) { - String value = getValue(name); + String value = getValue(name, ignoreDefaultValue); if (value != null) { value = resolve(value, name); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/AbstractIdeSubLogger.java b/cli/src/main/java/com/devonfw/tools/ide/log/AbstractIdeSubLogger.java index 08e630a9a..d9050540d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/AbstractIdeSubLogger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/AbstractIdeSubLogger.java @@ -25,6 +25,73 @@ public IdeLogLevel getLevel() { return this.level; } + /** + * Should only be used internally by logger implementation. + * + * @param message the message template. + * @param args the dynamic arguments to fill in. + * @return the resolved message with the parameters filled in. + */ + protected String compose(String message, Object... args) { + + int pos = message.indexOf("{}"); + if (pos < 0) { + if (args.length > 0) { + invalidMessage(message, false, args); + } + return message; + } + int argIndex = 0; + int start = 0; + int length = message.length(); + StringBuilder sb = new StringBuilder(length + 48); + while (pos >= 0) { + sb.append(message, start, pos); + sb.append(args[argIndex++]); + start = pos + 2; + pos = message.indexOf("{}", start); + if ((argIndex >= args.length) && (pos > 0)) { + invalidMessage(message, true, args); + pos = -1; + } + } + if (start < length) { + String rest = message.substring(start); + sb.append(rest); + } + if (argIndex < args.length) { + invalidMessage(message, false, args); + } + return sb.toString(); + } + + private void invalidMessage(String message, boolean more, Object[] args) { + + warning("Invalid log message with " + args.length + " argument(s) but " + (more ? "more" : "less") + + " placeholders: " + message); + } + + private void warning(String message) { + + boolean colored = isColored(); + if (colored) { + System.err.print(IdeLogLevel.ERROR.getEndColor()); + System.err.print(IdeLogLevel.ERROR.getStartColor()); + } + System.err.println(message); + if (colored) { + System.err.print(IdeLogLevel.ERROR.getEndColor()); + } + } + + /** + * @return {@code true} if colored logging is used, {@code false} otherwise. + */ + protected boolean isColored() { + + return false; + } + @Override public String toString() { diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogExceptionDetails.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogExceptionDetails.java new file mode 100644 index 000000000..de5434154 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogExceptionDetails.java @@ -0,0 +1,111 @@ +package com.devonfw.tools.ide.log; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * {@link Enum} with the available details logged for an {@link Throwable error}. + */ +enum IdeLogExceptionDetails { + + /** Log the entire stacktrace. */ + STACKTRACE(512) { + + @Override + void format(Throwable error, StringWriter sw) { + + try (PrintWriter pw = new PrintWriter(sw)) { + error.printStackTrace(pw); + } + } + }, + + /** Log only the exception type and message. */ + TO_STRING(32) { + + @Override + void format(Throwable error, StringWriter sw) { + + sw.append(error.toString()); + } + }, + + /** Log only the message. */ + MESSAGE(16) { + + @Override + void format(Throwable error, StringWriter sw) { + + String errorMessage = error.getMessage(); + if (isBlank(errorMessage)) { + errorMessage = error.getClass().getName(); + } + sw.append(errorMessage); + } + }; + + private final int capacityOffset; + + private IdeLogExceptionDetails(int capacityOffset) { + + this.capacityOffset = capacityOffset; + } + + /** + * @param message the formatted log message. + * @param error the {@link Throwable} to log. + */ + String format(String message, Throwable error) { + + boolean hasMessage = !isBlank(message); + if (error == null) { + if (hasMessage) { + return message; + } else { + return "Internal error: Both message and error is null - nothing to log!"; + } + } + int capacity = this.capacityOffset; + if (hasMessage) { + capacity = capacity + message.length() + 1; + } + StringWriter sw = new StringWriter(capacity); + if (hasMessage) { + sw.append(message); + sw.append('\n'); + } + format(error, sw); + return sw.toString(); + } + + abstract void format(Throwable error, StringWriter sw); + + private static boolean isBlank(String string) { + + if ((string == null) || (string.isBlank())) { + return true; + } + return false; + } + + /** + * @param level the {@link IdeLogLevel} of the {@link IdeSubLogger}. + * @param minLogLevel the minimum {@link IdeLogLevel} (threshold). + * @return the {@link IdeLogExceptionDetails}. + */ + static IdeLogExceptionDetails of(IdeLogLevel level, IdeLogLevel minLogLevel) { + + if ((minLogLevel == IdeLogLevel.TRACE) || (minLogLevel == IdeLogLevel.DEBUG)) { + return STACKTRACE; + } + switch (level) { + case ERROR: + return STACKTRACE; + case WARNING: + return TO_STRING; + default: + return MESSAGE; + } + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogger.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogger.java index 6c7115c75..ff3593ba3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogger.java @@ -1,287 +1,242 @@ -package com.devonfw.tools.ide.log; - -import java.io.PrintWriter; -import java.io.StringWriter; - -/** - * Interface for interaction with the user allowing to input and output information. - */ -public interface IdeLogger { - - /** - * @param level the {@link IdeLogLevel}. - * @return the requested {@link IdeLogLevel} for the given {@link IdeLogLevel}. - * @see IdeSubLogger#getLevel() - */ - IdeSubLogger level(IdeLogLevel level); - - /** - * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#TRACE}. - */ - default IdeSubLogger trace() { - - return level(IdeLogLevel.TRACE); - } - - /** - * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#TRACE}. - */ - default void trace(String message) { - - trace().log(message); - } - - /** - * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#TRACE}. - * @param args the dynamic arguments to fill in. - */ - default void trace(String message, Object... args) { - - trace().log(message, args); - } - - /** - * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#DEBUG}. - */ - default IdeSubLogger debug() { - - return level(IdeLogLevel.DEBUG); - } - - /** - * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#DEBUG}. - */ - default void debug(String message) { - - debug().log(message); - } - - /** - * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#DEBUG}. - * @param args the dynamic arguments to fill in. - */ - default void debug(String message, Object... args) { - - debug().log(message, args); - } - - /** - * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#INFO}. - */ - default IdeSubLogger info() { - - return level(IdeLogLevel.INFO); - } - - /** - * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#INFO}. - */ - default void info(String message) { - - info().log(message); - } - - /** - * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#INFO}. - * @param args the dynamic arguments to fill in. - */ - default void info(String message, Object... args) { - - info().log(message, args); - } - - /** - * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#STEP}. - */ - default IdeSubLogger step() { - - return level(IdeLogLevel.STEP); - } - - /** - * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#STEP}. - */ - default void step(String message) { - - step().log(message); - } - - /** - * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#STEP}. - * @param args the dynamic arguments to fill in. - */ - default void step(String message, Object... args) { - - step().log(message, args); - } - - /** - * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#INTERACTION}. - */ - default IdeSubLogger interaction() { - - return level(IdeLogLevel.INTERACTION); - } - - /** - * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#INTERACTION}. - */ - default void interaction(String message) { - - interaction().log(message); - } - - /** - * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#INTERACTION}. - * @param args the dynamic arguments to fill in. - */ - default void interaction(String message, Object... args) { - - interaction().log(message, args); - } - - /** - * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#SUCCESS}. - */ - default IdeSubLogger success() { - - return level(IdeLogLevel.SUCCESS); - } - - /** - * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#SUCCESS}. - */ - default void success(String message) { - - success().log(message); - } - - /** - * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#SUCCESS}. - * @param args the dynamic arguments to fill in. - */ - default void success(String message, Object... args) { - - success().log(message, args); - } - - /** - * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#WARNING}. - */ - default IdeSubLogger warning() { - - return level(IdeLogLevel.WARNING); - } - - /** - * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#WARNING}. - */ - default void warning(String message) { - - warning().log(message); - } - - /** - * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#WARNING}. - * @param args the dynamic arguments to fill in. - */ - default void warning(String message, Object... args) { - - warning().log(message, args); - } - - /** - * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#ERROR}. - */ - default IdeSubLogger error() { - - return level(IdeLogLevel.ERROR); - } - - /** - * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#ERROR}. - */ - default void error(String message) { - - error().log(message); - } - - /** - * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#ERROR}. - * @param args the dynamic arguments to fill in. - */ - default void error(String message, Object... args) { - - error().log(message, args); - } - - /** - * @param error the {@link Throwable} that caused the error. - */ - default void error(Throwable error) { - - error(error, null); - } - - /** - * @param error the {@link Throwable} that caused the error. - * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#ERROR}. - */ - default void error(Throwable error, String message) { - - boolean hasMessage = !isBlank(message); - if (error == null) { - if (hasMessage) { - error(message); - } else { - error("Internal error: Throwable is null!"); - } - return; - } - boolean traceEnabled = true; - boolean debugEnabled = debug().isEnabled(); - String errorMessage = error.getMessage(); - if (errorMessage == null) { - errorMessage = ""; - } else if (errorMessage.isBlank()) { - errorMessage = error.getClass().getName(); - } - int capacity = 0; - if (hasMessage) { - capacity = message.length(); - } else if (!debugEnabled) { - capacity = errorMessage.length(); - } - if (debugEnabled) { - capacity = capacity + 32 + errorMessage.length(); - } - if (traceEnabled) { - capacity = capacity + 512; - } - StringWriter sw = new StringWriter(capacity); - if (hasMessage) { - sw.append(message); - } - if (traceEnabled) { - sw.append('\n'); - try (PrintWriter pw = new PrintWriter(sw)) { - error.printStackTrace(pw); - } - } else if (debugEnabled) { - sw.append('\n'); - sw.append(error.toString()); - } else { - sw.append(errorMessage); - } - error(sw.toString()); - } - - private static boolean isBlank(String string) { - - if ((string == null) || (string.isBlank())) { - return true; - } - return false; - } - -} +package com.devonfw.tools.ide.log; + +/** + * Interface for interaction with the user allowing to input and output information. + */ +public interface IdeLogger { + + /** + * @param level the {@link IdeLogLevel}. + * @return the requested {@link IdeLogLevel} for the given {@link IdeLogLevel}. + * @see IdeSubLogger#getLevel() + */ + IdeSubLogger level(IdeLogLevel level); + + /** + * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#TRACE}. + */ + default IdeSubLogger trace() { + + return level(IdeLogLevel.TRACE); + } + + /** + * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#TRACE}. + */ + default void trace(String message) { + + trace().log(message); + } + + /** + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#TRACE}. + * @param args the dynamic arguments to fill in. + */ + default void trace(String message, Object... args) { + + trace().log(message, args); + } + + /** + * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#DEBUG}. + */ + default IdeSubLogger debug() { + + return level(IdeLogLevel.DEBUG); + } + + /** + * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#DEBUG}. + */ + default void debug(String message) { + + debug().log(message); + } + + /** + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#DEBUG}. + * @param args the dynamic arguments to fill in. + */ + default void debug(String message, Object... args) { + + debug().log(message, args); + } + + /** + * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#INFO}. + */ + default IdeSubLogger info() { + + return level(IdeLogLevel.INFO); + } + + /** + * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#INFO}. + */ + default void info(String message) { + + info().log(message); + } + + /** + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#INFO}. + * @param args the dynamic arguments to fill in. + */ + default void info(String message, Object... args) { + + info().log(message, args); + } + + /** + * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#STEP}. + */ + default IdeSubLogger step() { + + return level(IdeLogLevel.STEP); + } + + /** + * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#STEP}. + */ + default void step(String message) { + + step().log(message); + } + + /** + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#STEP}. + * @param args the dynamic arguments to fill in. + */ + default void step(String message, Object... args) { + + step().log(message, args); + } + + /** + * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#INTERACTION}. + */ + default IdeSubLogger interaction() { + + return level(IdeLogLevel.INTERACTION); + } + + /** + * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#INTERACTION}. + */ + default void interaction(String message) { + + interaction().log(message); + } + + /** + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#INTERACTION}. + * @param args the dynamic arguments to fill in. + */ + default void interaction(String message, Object... args) { + + interaction().log(message, args); + } + + /** + * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#SUCCESS}. + */ + default IdeSubLogger success() { + + return level(IdeLogLevel.SUCCESS); + } + + /** + * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#SUCCESS}. + */ + default void success(String message) { + + success().log(message); + } + + /** + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#SUCCESS}. + * @param args the dynamic arguments to fill in. + */ + default void success(String message, Object... args) { + + success().log(message, args); + } + + /** + * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#WARNING}. + */ + default IdeSubLogger warning() { + + return level(IdeLogLevel.WARNING); + } + + /** + * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#WARNING}. + */ + default void warning(String message) { + + warning().log(message); + } + + /** + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#WARNING}. + * @param args the dynamic arguments to fill in. + */ + default void warning(String message, Object... args) { + + warning().log(message, args); + } + + /** + * @return the {@link #level(IdeLogLevel) logger} for {@link IdeLogLevel#ERROR}. + */ + default IdeSubLogger error() { + + return level(IdeLogLevel.ERROR); + } + + /** + * @param message the {@link IdeSubLogger#log(String) message to log} with {@link IdeLogLevel#ERROR}. + */ + default void error(String message) { + + error().log(message); + } + + /** + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#ERROR}. + * @param args the dynamic arguments to fill in. + */ + default void error(String message, Object... args) { + + error().log(message, args); + } + + /** + * @param error the {@link Throwable} that caused the error. + */ + default void error(Throwable error) { + + error(error, null); + } + + /** + * @param error the {@link Throwable} that caused the error. + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#ERROR}. + */ + default void error(Throwable error, String message) { + + error().log(error, message); + } + + /** + * @param error the {@link Throwable} that caused the error. + * @param message the {@link IdeSubLogger#log(String, Object...) message to log} with {@link IdeLogLevel#ERROR}. + * @param args the dynamic arguments to fill in. + */ + default void error(Throwable error, String message, Object... args) { + + error().log(error, message, args); + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLogger.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLogger.java index dfbe5256f..1ae7a6ac8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLogger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLogger.java @@ -1,61 +1,56 @@ -package com.devonfw.tools.ide.log; - -/** - * Interface for a logger to {@link #log(String) log a message} on a specific {@link #getLevel() log-level}. - */ -public interface IdeSubLogger { - - /** - * @param message the message to log. - */ - void log(String message); - - /** - * @param message the message to log. Should contain "{}" as placeholder for the given arguments. - * @param args the dynamic arguments to fill in. - */ - default void log(String message, Object... args) { - - assert ((args.length == 0) - || !(args[0] instanceof Throwable)) : "Throwable has to be first argument before message!"; - if (!isEnabled()) { - return; - } - int pos = message.indexOf("{}"); - if ((pos < 0) || (args.length == 0)) { - log(message); - return; - } - int argIndex = 0; - int start = 0; - int length = message.length(); - StringBuilder sb = new StringBuilder(length + 48); - while (pos >= 0) { - sb.append(message, start, pos); - sb.append(args[argIndex++]); - start = pos + 2; - if (argIndex < args.length) { - pos = message.indexOf("{}", start); - } else { - pos = -1; - } - } - if (start < length) { - String rest = message.substring(start); - sb.append(rest); - } - log(sb.toString()); - } - - /** - * @return {@code true} if this logger is enabled, {@code false} otherwise (this logger does nothing and all - * {@link #log(String) logged messages} with be ignored). - */ - boolean isEnabled(); - - /** - * @return the {@link IdeLogLevel} of this logger. - */ - IdeLogLevel getLevel(); - -} +package com.devonfw.tools.ide.log; + +/** + * Interface for a logger to {@link #log(String) log a message} on a specific {@link #getLevel() log-level}. + */ +public interface IdeSubLogger { + + /** + * @param message the message to log. + */ + default void log(String message) { + + log(null, message); + } + + /** + * @param error the {@link Throwable} that was catched and should be logged or {@code null} for no error. + * @param message the message to log. + * @param args the dynamic arguments to fill in. + * @return the message headline that was logged. + */ + default String log(String message, Object... args) { + + return log(null, message, args); + } + + /** + * @param error the {@link Throwable} that was catched and should be logged or {@code null} for no error. + * @param message the message to log. + * @return the message headline that was logged. + */ + default String log(Throwable error, String message) { + + return log(error, message, (Object[]) null); + } + + /** + * @param error the {@link Throwable} that was catched and should be logged or {@code null} for no error. + * @param message the message to log. + * @param args the dynamic arguments to fill in. + * @return the message headline that was logged. + */ + String log(Throwable error, String message, Object... args); + + /** + * @return {@code true} if this logger is enabled, {@code false} otherwise (this logger does nothing and all + * {@link #log(String) logged messages} with be ignored). + */ + boolean isEnabled(); + + /** + * @return the {@link IdeLogLevel} of this logger. + */ + IdeLogLevel getLevel(); + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerNone.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerNone.java index e5d2cc50d..be8307fb2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerNone.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerNone.java @@ -1,29 +1,30 @@ -package com.devonfw.tools.ide.log; - -/** - * Implementation of {@link IdeSubLogger} that is NOT {@link #isEnabled() enabled} and does nothing. - */ -public final class IdeSubLoggerNone extends AbstractIdeSubLogger { - - /** - * The constructor. - * - * @param level the {@link #getLevel() log-level}. - */ - public IdeSubLoggerNone(IdeLogLevel level) { - - super(level); - } - - @Override - public void log(String message) { - - } - - @Override - public boolean isEnabled() { - - return false; - } - -} +package com.devonfw.tools.ide.log; + +/** + * Implementation of {@link IdeSubLogger} that is NOT {@link #isEnabled() enabled} and does nothing. + */ +public final class IdeSubLoggerNone extends AbstractIdeSubLogger { + + /** + * The constructor. + * + * @param level the {@link #getLevel() log-level}. + */ + public IdeSubLoggerNone(IdeLogLevel level) { + + super(level); + } + + @Override + public String log(Throwable error, String message, Object... args) { + + return message; + } + + @Override + public boolean isEnabled() { + + return false; + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerOut.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerOut.java index 734cbea7e..9a733a688 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerOut.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerOut.java @@ -12,14 +12,17 @@ public class IdeSubLoggerOut extends AbstractIdeSubLogger { private final boolean colored; + private final IdeLogExceptionDetails exceptionDetails; + /** * The constructor. * * @param level the {@link #getLevel() log-level}. * @param out the {@link Appendable} to {@link Appendable#append(CharSequence) write} log messages to. * @param colored - {@code true} for colored output according to {@link IdeLogLevel}, {@code false} otherwise. + * @param minLogLevel the minimum log level (threshold). */ - public IdeSubLoggerOut(IdeLogLevel level, Appendable out, boolean colored) { + public IdeSubLoggerOut(IdeLogLevel level, Appendable out, boolean colored, IdeLogLevel minLogLevel) { super(level); if (out == null) { @@ -33,6 +36,7 @@ public IdeSubLoggerOut(IdeLogLevel level, Appendable out, boolean colored) { this.out = out; } this.colored = colored; + this.exceptionDetails = IdeLogExceptionDetails.of(level, minLogLevel); } @Override @@ -41,6 +45,12 @@ public boolean isEnabled() { return true; } + @Override + protected boolean isColored() { + + return this.colored; + } + @Override public void log(String message) { @@ -62,4 +72,22 @@ public void log(String message) { } } + @Override + public String log(Throwable error, String message, Object... args) { + + if (args != null) { + message = compose(message, args); + } + log(this.exceptionDetails.format(message, error)); + if (message == null) { + if (error == null) { + return null; + } else { + return error.toString(); + } + } else { + return message; + } + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java index 16ba79150..72b0a3d4b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java @@ -30,6 +30,7 @@ public class ProcessContextImpl implements ProcessContext { private static final String PREFIX_USR_BIN_ENV = "/usr/bin/env "; + /** The owning {@link IdeContext}. */ protected final IdeContext context; private final ProcessBuilder processBuilder; @@ -74,7 +75,7 @@ public ProcessContext directory(Path directory) { if (directory != null) { this.processBuilder.directory(directory.toFile()); } else { - context.debug( + this.context.debug( "Could not set the process builder's working directory! Directory of the current java process is used."); } @@ -295,14 +296,10 @@ private String findBashOnWindows() { throw new IllegalStateException("Could not find Bash. Please install Git for Windows and rerun."); } - private String addExecutable(String executable, List args) { + private String addExecutable(String exec, List args) { - if (!SystemInfoImpl.INSTANCE.isWindows()) { - args.add(executable); - return null; - } String interpreter = null; - String fileExtension = FilenameUtil.getExtension(executable); + String fileExtension = FilenameUtil.getExtension(exec); boolean isBashScript = "sh".equals(fileExtension); if (!isBashScript) { String sheBang = getSheBang(this.executable); @@ -323,7 +320,6 @@ private String addExecutable(String executable, List args) { String bash = "bash"; interpreter = bash; // here we want to have native OS behavior even if OS is mocked during tests... - // if (this.context.getSystemInfo().isWindows()) { if (SystemInfoImpl.INSTANCE.isWindows()) { String findBashOnWindowsResult = findBashOnWindows(); if (findBashOnWindowsResult != null) { @@ -331,12 +327,11 @@ private String addExecutable(String executable, List args) { } } args.add(bash); + } else if (SystemInfoImpl.INSTANCE.isWindows() && "msi".equalsIgnoreCase(fileExtension)) { + args.add("msiexec"); + args.add("/i"); } - if ("msi".equalsIgnoreCase(fileExtension)) { - args.add(0, "/i"); - args.add(0, "msiexec"); - } - args.add(executable); + args.add(exec); return interpreter; } @@ -354,7 +349,7 @@ private void performLogOnError(ProcessResult result, int exitCode, String interp level = this.context.warning(); } else { level = this.context.error(); - level.log("Internal error: Undefined error handling {}", this.errorHandling); + this.context.error("Internal error: Undefined error handling {}", this.errorHandling); } level.log(message); } @@ -373,7 +368,7 @@ private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) { String bash = "bash"; // try to use bash in windows to start the process - if (context.getSystemInfo().isWindows()) { + if (this.context.getSystemInfo().isWindows()) { String findBashOnWindowsResult = findBashOnWindows(); if (findBashOnWindowsResult != null) { @@ -381,7 +376,7 @@ private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) { bash = findBashOnWindowsResult; } else { - context.warning( + this.context.warning( "Cannot start background process in windows! No bash installation found, output will be discarded."); this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD); return; @@ -400,7 +395,7 @@ private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) { private String buildCommandToRunInBackground() { - if (context.getSystemInfo().isWindows()) { + if (this.context.getSystemInfo().isWindows()) { StringBuilder stringBuilder = new StringBuilder(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResult.java b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResult.java index 56f59edf5..754aedef9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResult.java @@ -1,44 +1,59 @@ -package com.devonfw.tools.ide.process; - -import java.util.List; - -/** - * Result of a {@link Process} execution. - * - * @see ProcessContext#run() - */ -public interface ProcessResult { - - /** Exit code for success. */ - int SUCCESS = 0; - - /** Exit code if tool was requested that is not installed. */ - int TOOL_NOT_INSTALLED = 4; - - /** - * @return the exit code. Will be {@link #SUCCESS} on successful completion of the {@link Process}. - */ - int getExitCode(); - - /** - * @return {@code true} if the {@link #getExitCode() exit code} indicates {@link #SUCCESS}, {@code false} otherwise - * (an error occurred). - */ - default boolean isSuccessful() { - - return getExitCode() == SUCCESS; - } - - /** - * @return the {@link List} with the lines captured on standard out. Will be {@code null} if not captured but - * redirected. - */ - List getOut(); - - /** - * @return the {@link List} with the lines captured on standard error. Will be {@code null} if not captured but - * redirected. - */ - List getErr(); - -} +package com.devonfw.tools.ide.process; + +import java.util.List; + +/** + * Result of a {@link Process} execution. + * + * @see ProcessContext#run() + */ +public interface ProcessResult { + + /** Return code for success. */ + int SUCCESS = 0; + + /** Return code if tool was requested that is not installed. */ + int TOOL_NOT_INSTALLED = 4; + + /** + * Return code to abort gracefully. + * + * @see com.devonfw.tools.ide.cli.CliAbortException + */ + int ABORT = 22; + + /** + * Return code if {@link com.devonfw.tools.ide.context.IdeContext#isOffline() offline} but network is required for + * requested operation. + * + * @see com.devonfw.tools.ide.cli.CliOfflineException + */ + int OFFLINE = 23; + + /** + * @return the exit code. Will be {@link #SUCCESS} on successful completion of the {@link Process}. + */ + int getExitCode(); + + /** + * @return {@code true} if the {@link #getExitCode() exit code} indicates {@link #SUCCESS}, {@code false} otherwise + * (an error occurred). + */ + default boolean isSuccessful() { + + return getExitCode() == SUCCESS; + } + + /** + * @return the {@link List} with the lines captured on standard out. Will be {@code null} if not captured but + * redirected. + */ + List getOut(); + + /** + * @return the {@link List} with the lines captured on standard error. Will be {@code null} if not captured but + * redirected. + */ + List getErr(); + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/step/Step.java b/cli/src/main/java/com/devonfw/tools/ide/step/Step.java new file mode 100644 index 000000000..3f7823753 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/step/Step.java @@ -0,0 +1,199 @@ +package com.devonfw.tools.ide.step; + +/** + * Interface for a {@link Step} of the process. Allows to split larger processes into smaller steps that are traced + * and measured. At the end you can get a report with the hierarchy of all steps and their success/failure status, + * duration in absolute and relative numbers to gain transparency.
+ * The typical use should follow this pattern: + * + *

+ * Step step = context.{@link com.devonfw.tools.ide.context.IdeContext#newStep(String) newStep}("My step description");
+ * try {
+ *   // ... do something ...
+ *   step.{@link #success(String) success}("Did something successfully.");
+ * } catch (Exception e) {
+ *   step.{@link #error(Throwable, String)}(e, "Failed to do something.");
+ * } finally {
+ *   step.{@link #end() end()};
+ * }
+ * 
+ */ +public interface Step { + + /** Empty object array for no parameters. */ + Object[] NO_PARAMS = new Object[0]; + + /** + * @return the name of this {@link Step} as given to constructor. + */ + String getName(); + + /** + * @return the duration of this {@link Step} from construction to {@link #success()} or {@link #end()}. Will be + * {@code 0} if not {@link #end() ended}. + */ + long getDuration(); + + /** + * @return {@code Boolean#TRUE} if this {@link Step} has {@link #success() succeeded}, {@code Boolean#FALSE} if the + * {@link Step} has {@link #end() ended} without {@link #success() success} and {@code null} if the + * {@link Step} is still running. + */ + Boolean getSuccess(); + + /** + * @return {@code true} if this step completed {@link #success() successfully}, {@code false} otherwise. + */ + default boolean isSuccess() { + + return Boolean.TRUE.equals(getSuccess()); + } + + /** + * @return {@code true} if this step {@link #end() ended} without {@link #success() success} e.g. with an + * {@link #error(String) error}, {@code false} otherwise. + */ + default boolean isFailure() { + + return Boolean.FALSE.equals(getSuccess()); + } + + /** + * @return {@code true} if this step is silent and not logged by default, {@code false} otherwise (default). + */ + boolean isSilent(); + + /** + * Should be called to end this {@link Step} {@link #getSuccess() successfully}. May be called only once. + */ + default void success() { + + success(null); + } + + /** + * Should be called to end this {@link Step} {@link #getSuccess() successfully}. May be called only once. + * + * @param message the explicit message to log as success. + */ + default void success(String message) { + + success(message, (Object[]) null); + } + + /** + * Should be called to end this {@link Step} {@link #getSuccess() successfully}. May be called only once. + * + * @param message the explicit message to log as success. + * @param args the optional arguments to fill as placeholder into the {@code message}. + */ + void success(String message, Object... args); + + /** + * Ensures this {@link Step} is properly ended. Has to be called from a finally block. + */ + void end(); + + /** + * Should be called to end this {@link Step} as {@link #isFailure() failure} with an explicit error message. May be + * called only once. + * + * @param message the explicit message to log as error. + */ + default void error(String message) { + + error(null, message); + } + + /** + * Should be called to end this {@link Step} as {@link #isFailure() failure} with an explicit error message and/or + * {@link Throwable exception}. May be called only once. + * + * @param message the explicit message to log as error. + * @param args the optional arguments to fill as placeholder into the {@code message}. + */ + default void error(String message, Object... args) { + + error(null, message, args); + } + + /** + * Should be called to end this {@link Step} as {@link #isFailure() failure} with an explicit error message and/or + * {@link Throwable exception}. May be called only once. + * + * @param error the catched {@link Throwable}. + */ + default void error(Throwable error) { + + error(error, false); + } + + /** + * Should be called to end this {@link Step} as {@link #isFailure() failure} with an explicit error message and/or + * {@link Throwable exception}. May be called only once. + * + * @param error the catched {@link Throwable}. + * @param suppress to suppress the error logging (if error will be rethrown and duplicated error messages shall be + * avoided). + */ + default void error(Throwable error, boolean suppress) { + + assert (error != null); + error(error, suppress, null, (Object[]) null); + } + + /** + * Should be called to end this {@link Step} as {@link #isFailure() failure} with an explicit error message and/or + * {@link Throwable exception}. May be called only once. + * + * @param error the catched {@link Throwable}. May be {@code null} if only a {@code message} is provided. + * @param message the explicit message to log as error. + */ + default void error(Throwable error, String message) { + + error(error, message, (Object[]) null); + } + + /** + * Should be called to end this {@link Step} as {@link #isFailure() failure} with an explicit error message and/or + * {@link Throwable exception}. May be called only once. + * + * @param error the catched {@link Throwable}. May be {@code null} if only a {@code message} is provided. + * @param message the explicit message to log as error. + * @param args the optional arguments to fill as placeholder into the {@code message}. + */ + default void error(Throwable error, String message, Object... args) { + + error(error, false, message, args); + } + + /** + * Should be called to end this {@link Step} as {@link #isFailure() failure} with an explicit error message and/or + * {@link Throwable exception}. May be called only once. + * + * @param error the catched {@link Throwable}. May be {@code null} if only a {@code message} is provided. + * @param suppress to suppress the error logging (if error will be rethrown and duplicated error messages shall be + * avoided). + * @param message the explicit message to log as error. + * @param args the optional arguments to fill as placeholder into the {@code message}. + */ + void error(Throwable error, boolean suppress, String message, Object... args); + + /** + * @return the parent {@link Step} or {@code null} if there is no parent. + */ + Step getParent(); + + /** + * @param i the index of the requested parameter. Should be in the range from {@code 0} to + * {@link #getParameterCount()}-1. + * @return the parameter at the given index {@code i} or {@code null} if no such parameter exists. + */ + Object getParameter(int i); + + /** + * @return the number of {@link #getParameter(int) parameters}. + */ + int getParameterCount(); + +} \ No newline at end of file diff --git a/cli/src/main/java/com/devonfw/tools/ide/step/StepImpl.java b/cli/src/main/java/com/devonfw/tools/ide/step/StepImpl.java new file mode 100644 index 000000000..41dd493dc --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/step/StepImpl.java @@ -0,0 +1,289 @@ +package com.devonfw.tools.ide.step; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.devonfw.tools.ide.context.AbstractIdeContext; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.log.IdeSubLogger; + +/** + * Regular implementation of {@link Step}. + */ +public final class StepImpl implements Step { + + private final AbstractIdeContext context; + + private final StepImpl parent; + + private final String name; + + private final Object[] params; + + private final List children; + + private final long start; + + private final boolean silent; + + private Boolean success; + + private String errorMessage; + + private long duration; + + /** + * Creates and starts a new {@link StepImpl}. + * + * @param context the {@link IdeContext}. + * @param parent the {@link #getParent() parent step}. + * @param name the {@link #getName() step name}. + * @param silent the {@link #isSilent() silent flag}. + * @param params the parameters. Should have reasonable {@link Object#toString() string representations}. + */ + public StepImpl(AbstractIdeContext context, StepImpl parent, String name, boolean silent, Object... params) { + + super(); + this.context = context; + this.parent = parent; + this.name = name; + this.params = params; + this.silent = silent; + this.children = new ArrayList<>(); + this.start = System.currentTimeMillis(); + if (parent != null) { + parent.children.add(this); + } + if (params.length == 0) { + this.context.trace("Starting step {}...", name); + } else { + this.context.trace("Starting step {} with params {}...", name, Arrays.toString(params)); + } + if (!this.silent) { + this.context.step("Start: {}", name); + } + } + + @Override + public StepImpl getParent() { + + return this.parent; + } + + @Override + public String getName() { + + return this.name; + } + + @Override + public Object getParameter(int i) { + + if ((i < 0) || (i >= this.params.length)) { + return null; + } + return this.params[i]; + } + + @Override + public int getParameterCount() { + + return this.params.length; + } + + @Override + public boolean isSilent() { + + return this.silent; + } + + @Override + public long getDuration() { + + return this.duration; + } + + @Override + public Boolean getSuccess() { + + return this.success; + } + + @Override + public void success(String message, Object... args) { + + end(Boolean.TRUE, null, false, message, args); + } + + @Override + public void error(Throwable error, boolean suppress, String message, Object... args) { + + end(Boolean.FALSE, error, suppress, message, args); + } + + @Override + public void end() { + + end(null, null, false, null, null); + } + + private void end(Boolean newSuccess, Throwable error, boolean suppress, String message, Object[] args) { + + if (this.success != null) { + assert (this.duration > 0); + // success or error may only be called once per Step, while end() will be called again in finally block + assert (newSuccess == null) : "Step " + this.name + " already ended with " + this.success + + " and cannot be ended again with " + newSuccess; + return; + } + assert (this.duration == 0); + long delay = System.currentTimeMillis() - this.start; + if (delay == 0) { + delay = 1; + } + this.duration = delay; + if (newSuccess == null) { + newSuccess = Boolean.FALSE; + } + this.success = newSuccess; + if (newSuccess.booleanValue()) { + assert (error == null); + if (message != null) { + this.context.success(message, args); + } else if (!this.silent) { + this.context.success(this.name); + } + this.context.debug("Step '{}' ended successfully.", this.name); + } else { + IdeSubLogger logger; + if ((message != null) || (error != null)) { + if (suppress) { + if (error != null) { + this.errorMessage = error.toString(); + } else { + this.errorMessage = message; + } + } else { + this.errorMessage = this.context.error().log(error, message, args); + } + logger = this.context.debug(); + } else { + logger = this.context.info(); + } + logger.log("Step '{}' ended with failure.", this.name); + } + this.context.endStep(this); + } + + /** + * Logs the summary of this {@link Step}. Should typically only be called on the top-level {@link Step}. + * + * @param suppressSuccess - {@code true} to suppress the success message, {@code false} otherwise. + */ + public void logSummary(boolean suppressSuccess) { + + if (this.context.trace().isEnabled()) { + this.context.trace(toString()); + } + if (this.context.isQuietMode()) { + return; + } + StepSummary summary = new StepSummary(); + logErrorSummary(0, summary); + if (summary.getError() == 0) { + if (!suppressSuccess) { + this.context.success("Successfully completed {}", getNameWithParams()); + } + } else { + this.context.error(summary.toString()); + } + } + + private void logErrorSummary(int depth, StepSummary summary) { + + boolean failure = isFailure(); + summary.add(failure); + if (failure) { + this.context.error("{}Step '{}' failed: {}", getIndent(depth), getNameWithParams(), this.errorMessage); + } + depth++; + for (StepImpl child : this.children) { + child.logErrorSummary(depth, summary); + } + } + + private String getNameWithParams() { + + if ((this.params == null) || (this.params.length == 0)) { + return this.name; + } + StringBuilder sb = new StringBuilder(this.name.length() + 3 + this.params.length * 6); + getNameWithParams(sb); + return sb.toString(); + } + + private void getNameWithParams(StringBuilder sb) { + + sb.append(this.name); + sb.append(" ("); + String seperator = ""; + if (this.params != null) { + for (Object param : this.params) { + sb.append(seperator); + sb.append(param); + seperator = ","; + } + } + sb.append(')'); + } + + private void append(int depth, long totalDuration, long parentDuration, StringBuilder sb) { + + // indent + sb.append(getIndent(depth)); + getNameWithParams(sb); + sb.append(' '); + if (this.success == null) { + sb.append("is still running or was not properly ended due to programming error not using finally block "); + } else { + if (this.success.booleanValue()) { + sb.append("succeeded after "); + } else { + sb.append("failed after "); + } + sb.append(Duration.ofMillis(this.duration)); + } + if (this.duration < totalDuration) { + sb.append(" "); + double percentageBase = this.duration * 100; + double totalPercentage = percentageBase / totalDuration; + sb.append(totalPercentage); + sb.append("% of total "); + if (parentDuration < totalDuration) { + double parentPercentage = percentageBase / parentDuration; + sb.append(parentPercentage); + sb.append("% of parent"); + } + } + sb.append('\n'); + int childDepth = depth + 1; + for (StepImpl child : this.children) { + child.append(childDepth, totalDuration, this.duration, sb); + } + } + + private String getIndent(int depth) { + + return " ".repeat(depth); + } + + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(4096); + append(0, this.duration, this.duration, sb); + return sb.toString(); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/step/StepSummary.java b/cli/src/main/java/com/devonfw/tools/ide/step/StepSummary.java new file mode 100644 index 000000000..cdde37eeb --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/step/StepSummary.java @@ -0,0 +1,47 @@ +package com.devonfw.tools.ide.step; + +/** + * Simple container for the overall summary of a {@link Step}. + * + * @see StepImpl#logSummary(boolean) + */ +class StepSummary { + + private int total; + + private int error; + + /** + * @return the total number of {@link Step}s that had been executed. + */ + public int getTotal() { + + return this.total; + } + + /** + * @return the number of {@link Step}s that failed. + */ + public int getError() { + + return this.error; + } + + /** + * @param failure - see {@link Step#isFailure()}. + */ + public void add(boolean failure) { + + this.total++; + if (failure) { + this.error++; + } + } + + @Override + public String toString() { + + return this.error + " step(s) failed out of " + this.total + " steps."; + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java index c0b339413..83c0cd759 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java @@ -1,19 +1,20 @@ package com.devonfw.tools.ide.tool; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Set; + import com.devonfw.tools.ide.common.Tag; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.io.FileCopyMode; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.repo.ToolRepository; +import com.devonfw.tools.ide.step.Step; import com.devonfw.tools.ide.version.VersionIdentifier; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.Set; - /** * {@link ToolCommandlet} that is installed locally into the IDE. */ @@ -25,7 +26,7 @@ public abstract class LocalToolCommandlet extends ToolCommandlet { * @param context the {@link IdeContext}. * @param tool the {@link #getName() tool name}. * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} - * method. + * method. */ public LocalToolCommandlet(IdeContext context, String tool, Set tags) { @@ -42,13 +43,13 @@ public Path getToolPath() { /** * @return the {@link Path} where the executables of the tool can be found. Typically a "bin" folder inside - * {@link #getToolPath() tool path}. + * {@link #getToolPath() tool path}. */ public Path getToolBinPath() { Path toolPath = getToolPath(); - Path binPath = this.context.getFileAccess() - .findFirst(toolPath, path -> path.getFileName().toString().equals("bin"), false); + Path binPath = this.context.getFileAccess().findFirst(toolPath, path -> path.getFileName().toString().equals("bin"), + false); if ((binPath != null) && Files.isDirectory(binPath)) { return binPath; } @@ -61,34 +62,43 @@ protected boolean doInstall(boolean silent) { VersionIdentifier configuredVersion = getConfiguredVersion(); // get installed version before installInRepo actually may install the software VersionIdentifier installedVersion = getInstalledVersion(); - // install configured version of our tool in the software repository if not already installed - ToolInstallation installation = installInRepo(configuredVersion); - - // check if we already have this version installed (linked) locally in IDE_HOME/software - VersionIdentifier resolvedVersion = installation.resolvedVersion(); - if (resolvedVersion.equals(installedVersion) && !installation.newInstallation()) { - IdeLogLevel level = silent ? IdeLogLevel.DEBUG : IdeLogLevel.INFO; - this.context.level(level) - .log("Version {} of tool {} is already installed", installedVersion, getToolWithEdition()); - return false; - } - // we need to link the version or update the link. - Path toolPath = getToolPath(); - FileAccess fileAccess = this.context.getFileAccess(); - if (Files.exists(toolPath)) { - fileAccess.backup(toolPath); - } - fileAccess.mkdirs(toolPath.getParent()); - fileAccess.symlink(installation.linkDir(), toolPath); - this.context.getPath().setPath(this.tool, installation.binDir()); - if (installedVersion == null) { - this.context.success("Successfully installed {} in version {}", this.tool, resolvedVersion); - } else { - this.context.success("Successfully installed {} in version {} replacing previous version {}", this.tool, - resolvedVersion, installedVersion); + Step step = this.context.newStep(silent, "Install " + this.tool, configuredVersion); + try { + // install configured version of our tool in the software repository if not already installed + ToolInstallation installation = installInRepo(configuredVersion); + // check if we already have this version installed (linked) locally in IDE_HOME/software + VersionIdentifier resolvedVersion = installation.resolvedVersion(); + if (resolvedVersion.equals(installedVersion) && !installation.newInstallation()) { + IdeLogLevel level = silent ? IdeLogLevel.DEBUG : IdeLogLevel.INFO; + this.context.level(level).log("Version {} of tool {} is already installed", installedVersion, + getToolWithEdition()); + step.success(); + return false; + } + // we need to link the version or update the link. + Path toolPath = getToolPath(); + FileAccess fileAccess = this.context.getFileAccess(); + if (Files.exists(toolPath)) { + fileAccess.backup(toolPath); + } + fileAccess.mkdirs(toolPath.getParent()); + fileAccess.symlink(installation.linkDir(), toolPath); + this.context.getPath().setPath(this.tool, installation.binDir()); + if (installedVersion == null) { + step.success("Successfully installed {} in version {}", this.tool, resolvedVersion); + } else { + step.success("Successfully installed {} in version {} replacing previous version {}", this.tool, + resolvedVersion, installedVersion); + } + postInstall(); + return true; + } catch (RuntimeException e) { + step.error(e, true); + throw e; + } finally { + step.end(); } - postInstall(); - return true; + } /** @@ -97,7 +107,7 @@ protected boolean doInstall(boolean silent) { * IDE installation. * * @param version the {@link VersionIdentifier} requested to be installed. May also be a - * {@link VersionIdentifier#isPattern() version pattern}. + * {@link VersionIdentifier#isPattern() version pattern}. * @return the {@link ToolInstallation} in the central software repository matching the given {@code version}. */ public ToolInstallation installInRepo(VersionIdentifier version) { @@ -111,7 +121,7 @@ public ToolInstallation installInRepo(VersionIdentifier version) { * IDE installation. * * @param version the {@link VersionIdentifier} requested to be installed. May also be a - * {@link VersionIdentifier#isPattern() version pattern}. + * {@link VersionIdentifier#isPattern() version pattern}. * @param edition the specific edition to install. * @return the {@link ToolInstallation} in the central software repository matching the given {@code version}. */ @@ -126,7 +136,7 @@ public ToolInstallation installInRepo(VersionIdentifier version, String edition) * IDE installation. * * @param version the {@link VersionIdentifier} requested to be installed. May also be a - * {@link VersionIdentifier#isPattern() version pattern}. + * {@link VersionIdentifier#isPattern() version pattern}. * @param edition the specific edition to install. * @param toolRepository the {@link ToolRepository} to use. * @return the {@link ToolInstallation} in the central software repository matching the given {@code version}. @@ -191,8 +201,8 @@ private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier } if (linkDir != rootDir) { assert (!linkDir.equals(rootDir)); - this.context.getFileAccess() - .copy(toolVersionFile, linkDir.resolve(IdeContext.FILE_SOFTWARE_VERSION), FileCopyMode.COPY_FILE_OVERRIDE); + this.context.getFileAccess().copy(toolVersionFile, linkDir.resolve(IdeContext.FILE_SOFTWARE_VERSION), + FileCopyMode.COPY_FILE_OVERRIDE); } return new ToolInstallation(rootDir, linkDir, binDir, resolvedVersion, newInstallation); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java index ea9f7656b..be5a5ee10 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java @@ -1,5 +1,11 @@ package com.devonfw.tools.ide.tool; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.common.Tag; import com.devonfw.tools.ide.common.Tags; @@ -13,12 +19,6 @@ import com.devonfw.tools.ide.property.StringListProperty; import com.devonfw.tools.ide.version.VersionIdentifier; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Set; - /** * {@link Commandlet} for a tool integrated into the IDE. */ @@ -40,7 +40,7 @@ public abstract class ToolCommandlet extends Commandlet implements Tags { * @param context the {@link IdeContext}. * @param tool the {@link #getName() tool name}. * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} - * method. + * method. */ public ToolCommandlet(IdeContext context, String tool, Set tags) { @@ -94,8 +94,8 @@ public void run() { * * @param processMode see {@link ProcessMode} * @param toolVersion the explicit version (pattern) to run. Typically {@code null} to ensure the configured version - * is installed and use that one. Otherwise, the specified version will be installed in the software repository - * without touching and IDE installation and used to run. + * is installed and use that one. Otherwise, the specified version will be installed in the software repository + * without touching and IDE installation and used to run. * @param args the command-line arguments to run the tool. */ public void runTool(ProcessMode processMode, VersionIdentifier toolVersion, String... args) { @@ -134,7 +134,7 @@ public String getEdition() { /** * @return the {@link #getName() tool} with its {@link #getEdition() edition}. The edition will be omitted if same as - * tool. + * tool. * @see #getToolWithEdition(String, String) */ protected final String getToolWithEdition() { @@ -146,7 +146,7 @@ protected final String getToolWithEdition() { * @param tool the tool name. * @param edition the edition. * @return the {@link #getName() tool} with its {@link #getEdition() edition}. The edition will be omitted if same as - * tool. + * tool. */ protected final static String getToolWithEdition(String tool, String edition) { @@ -169,7 +169,7 @@ public VersionIdentifier getConfiguredVersion() { * {@link com.devonfw.tools.ide.commandlet.Commandlet}s. * * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and - * nothing has changed. + * nothing has changed. */ public boolean install() { @@ -182,7 +182,7 @@ public boolean install() { * * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise. * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and - * nothing has changed. + * nothing has changed. */ public boolean install(boolean silent) { @@ -194,7 +194,7 @@ public boolean install(boolean silent) { * * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise. * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and - * nothing has changed. + * nothing has changed. */ protected abstract boolean doInstall(boolean silent); @@ -271,7 +271,7 @@ public String getInstalledEdition() { /** * @param toolPath the installation {@link Path} where to find currently installed tool. The name of the parent - * directory of the real path corresponding to the passed {@link Path path} must be the name of the edition. + * directory of the real path corresponding to the passed {@link Path path} must be the name of the edition. * @return the installed edition of this tool or {@code null} if not installed. */ public String getInstalledEdition(Path toolPath) { @@ -287,11 +287,10 @@ public String getInstalledEdition(Path toolPath) { } return edition; } catch (IOException e) { - throw new IllegalStateException( - "Couldn't determine the edition of " + getName() + " from the directory structure of its software path " - + toolPath - + ", assuming the name of the parent directory of the real path of the software path to be the edition " - + "of the tool.", e); + throw new IllegalStateException("Couldn't determine the edition of " + getName() + + " from the directory structure of its software path " + toolPath + + ", assuming the name of the parent directory of the real path of the software path to be the edition " + + "of the tool.", e); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/docker/Docker.java b/cli/src/main/java/com/devonfw/tools/ide/tool/docker/Docker.java index 0119a3d32..7de426203 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/docker/Docker.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/docker/Docker.java @@ -14,6 +14,11 @@ import com.devonfw.tools.ide.tool.PackageManager; import com.devonfw.tools.ide.version.VersionIdentifier; +/** + * {@link GlobalToolCommandlet} for docker either as + * Rancher Desktop or as + * Docker Desktop. + */ public class Docker extends GlobalToolCommandlet { /** * The constructor. @@ -28,9 +33,9 @@ public Docker(IdeContext context) { @Override public boolean isExtract() { - return switch (context.getSystemInfo().getOs()) { + return switch (this.context.getSystemInfo().getOs()) { case WINDOWS -> false; - case MAC -> context.getSystemInfo().getArchitecture().equals(SystemArchitecture.ARM64); + case MAC -> this.context.getSystemInfo().getArchitecture().equals(SystemArchitecture.ARM64); case LINUX -> true; }; } @@ -38,7 +43,7 @@ public boolean isExtract() { @Override protected boolean doInstall(boolean silent) { - if (context.getSystemInfo().isLinux()) { + if (this.context.getSystemInfo().isLinux()) { return installWithPackageManger(getPackageMangerCommands(), silent); } else { return super.doInstall(silent); @@ -70,7 +75,7 @@ private Map> getPackageMangerCommands() { @Override protected String getBinaryName() { - if (context.getSystemInfo().isLinux()) { + if (this.context.getSystemInfo().isLinux()) { return "rancher-desktop"; } else { return super.getBinaryName(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/variable/AbstractVariableDefinition.java b/cli/src/main/java/com/devonfw/tools/ide/variable/AbstractVariableDefinition.java index a83eefccf..bfcd6d3ee 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/variable/AbstractVariableDefinition.java +++ b/cli/src/main/java/com/devonfw/tools/ide/variable/AbstractVariableDefinition.java @@ -1,10 +1,11 @@ package com.devonfw.tools.ide.variable; -import com.devonfw.tools.ide.context.IdeContext; - import java.util.Objects; import java.util.function.Function; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; + /** * Abstract base implementation of {@link VariableDefinition}. * @@ -51,8 +52,7 @@ public AbstractVariableDefinition(String name, String legacyName) { * * @param name the {@link #getName() variable name}. * @param legacyName the {@link #getLegacyName() legacy name}. - * @param defaultValueFactory the factory {@link Function} for the - * {@link #getDefaultValue(IdeContext) default value}. + * @param defaultValueFactory the factory {@link Function} for the {@link #getDefaultValue(IdeContext) default value}. * @param forceDefaultValue the {@link #isForceDefaultValue() forceDefaultValue} flag. */ public AbstractVariableDefinition(String name, String legacyName, Function defaultValueFactory, @@ -66,8 +66,7 @@ public AbstractVariableDefinition(String name, String legacyName, Function defaultValueFactory) { @@ -79,8 +78,7 @@ public AbstractVariableDefinition(String name, String legacyName, Function List.of("mvn", "npm")); /** {@link VariableDefinition} for list of IDE tools to create start scripts for. */ VariableDefinitionStringList CREATE_START_SCRIPTS = new VariableDefinitionStringList("CREATE_START_SCRIPTS", diff --git a/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinition.java b/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinition.java index 1124a63e9..1822c25dd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinition.java +++ b/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinition.java @@ -1,11 +1,12 @@ package com.devonfw.tools.ide.variable; +import java.nio.file.Path; +import java.util.Collection; + import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.VariableLine; -import java.nio.file.Path; - /** * Interface for a definition of a variable. * @@ -20,7 +21,7 @@ public interface VariableDefinition { /** * @return the optional legacy name that is still supported for downward compatibility. May be {@code null} if - * undefined (no legacy support). + * undefined (no legacy support). */ String getLegacyName(); @@ -52,8 +53,8 @@ default String getDefaultValueAsString(IdeContext context) { /** * @return {@code true} if the {@link #getDefaultValue(IdeContext) default value} shall be used without any - * {@link EnvironmentVariables#get(String) variable lookup} (to prevent odd overriding of build in variables like - * IDE_HOME), {@code false} otherwise (overriding of default value is allowed and intended). + * {@link EnvironmentVariables#get(String) variable lookup} (to prevent odd overriding of build in variables + * like IDE_HOME), {@code false} otherwise (overriding of default value is allowed and intended). */ boolean isForceDefaultValue(); @@ -79,6 +80,15 @@ default String toString(V value) { return ""; } else if (value instanceof Path) { return value.toString().replace('\\', '/'); + } else if (value instanceof Collection collection) { + StringBuilder sb = new StringBuilder(); + for (Object element : collection) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(element); + } + return sb.toString(); } return value.toString(); } 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 4eec0653a..c145d86fb 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,109 +1,125 @@ -package com.devonfw.tools.ide.variable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; - -import com.devonfw.tools.ide.context.IdeContext; -import com.devonfw.tools.ide.environment.VariableLine; - -/** - * Implementation of {@link VariableDefinition} for a variable with the {@link #getValueType() value type} - * {@link String}. - */ -public class VariableDefinitionStringList extends AbstractVariableDefinition> { - - /** - * The constructor. - * - * @param name the {@link #getName() variable name}. - */ - public VariableDefinitionStringList(String name) { - - super(name); - } - - /** - * The constructor. - * - * @param name the {@link #getName() variable name}. - * @param legacyName the {@link #getLegacyName() legacy name}. - */ - public VariableDefinitionStringList(String name, String legacyName) { - - super(name, legacyName); - } - - /** - * The constructor. - * - * @param name the {@link #getName() variable name}. - * @param legacyName the {@link #getLegacyName() legacy name}. - * @param defaultValueFactory the factory {@link Function} for the {@link #getDefaultValue(IdeContext) default value}. - */ - public VariableDefinitionStringList(String name, String legacyName, - Function> defaultValueFactory) { - - super(name, legacyName, defaultValueFactory); - } - - /** - * The constructor. - * - * @param name the {@link #getName() variable name}. - * @param legacyName the {@link #getLegacyName() legacy name}. - * @param defaultValueFactory the factory {@link Function} for the {@link #getDefaultValue(IdeContext) default value}. - * @param forceDefaultValue the {@link #isForceDefaultValue() forceDefaultValue} flag. - */ - public VariableDefinitionStringList(String name, String legacyName, - Function> defaultValueFactory, boolean forceDefaultValue) { - - super(name, legacyName, defaultValueFactory, forceDefaultValue); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public Class> getValueType() { - - return (Class) List.class; - } - - @Override - 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 = Collections.unmodifiableList(list); - return list; - } - - private boolean isBashArray(String value) { - - return value.startsWith("(") && value.endsWith(")"); - } - - @Override - public VariableLine migrateLine(VariableLine line) { - - line = super.migrateLine(line); - String value = line.getValue(); - if ((value != null) && isBashArray(value)) { - List list = fromString(value, null); - line = line.withValue(String.join(", ", list)); - } - return line; - } -} +package com.devonfw.tools.ide.variable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.VariableLine; + +/** + * Implementation of {@link VariableDefinition} for a variable with the {@link #getValueType() value type} + * {@link String}. + */ +public class VariableDefinitionStringList extends AbstractVariableDefinition> { + + /** + * The constructor. + * + * @param name the {@link #getName() variable name}. + */ + public VariableDefinitionStringList(String name) { + + super(name); + } + + /** + * The constructor. + * + * @param name the {@link #getName() variable name}. + * @param legacyName the {@link #getLegacyName() legacy name}. + */ + public VariableDefinitionStringList(String name, String legacyName) { + + super(name, legacyName); + } + + /** + * The constructor. + * + * @param name the {@link #getName() variable name}. + * @param legacyName the {@link #getLegacyName() legacy name}. + * @param defaultValueFactory the factory {@link Function} for the {@link #getDefaultValue(IdeContext) default value}. + */ + public VariableDefinitionStringList(String name, String legacyName, + Function> defaultValueFactory) { + + super(name, legacyName, defaultValueFactory); + } + + /** + * The constructor. + * + * @param name the {@link #getName() variable name}. + * @param legacyName the {@link #getLegacyName() legacy name}. + * @param defaultValueFactory the factory {@link Function} for the {@link #getDefaultValue(IdeContext) default value}. + * @param forceDefaultValue the {@link #isForceDefaultValue() forceDefaultValue} flag. + */ + public VariableDefinitionStringList(String name, String legacyName, + Function> defaultValueFactory, boolean forceDefaultValue) { + + super(name, legacyName, defaultValueFactory, forceDefaultValue); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Class> getValueType() { + + return (Class) List.class; + } + + @Override + 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 = Collections.unmodifiableList(list); + return list; + } + + private boolean isBashArray(String value) { + + return value.startsWith("(") && value.endsWith(")"); + } + + @Override + public VariableLine migrateLine(VariableLine line) { + + line = super.migrateLine(line); + String value = line.getValue(); + if ((value != null) && isBashArray(value)) { + List list = fromString(value, null); + line = line.withValue(String.join(", ", list)); + } + return line; + } + + @Override + public String toString(List value) { + + if (value == null) { + return ""; + } + StringBuilder sb = new StringBuilder(value.size() * 5); + for (Object element : value) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(element); + } + return sb.toString(); + } +} diff --git a/cli/src/main/package/bin/ide b/cli/src/main/package/bin/ide index a0c459297..e5a05802a 100644 --- a/cli/src/main/package/bin/ide +++ b/cli/src/main/package/bin/ide @@ -1,18 +1,28 @@ #!/usr/bin/env bash -# Call native ideasy with user-provided arguments -ideasy "$@" -return_code=$? -if [ $return_code -ne 0 ]; then # Check if ideasy exit code is not equal to 0 - echo -e "\n\033[91mError: IDEasy failed with exit code $return_code \033[91m" >&2 # Print error message to stderr - return $return_code # Return the same error code as ideasy +_IDEASY="$(dirname "${BASH_SOURCE}")/ideasy" +if [ $# != 0 ]; then + "${_IDEASY}" "$@" + return_code=$? + if [ $return_code != 0 ]; then + echo -e "\n\033[91mError: IDEasy failed with exit code ${return_code}\033[91m" >&2 + unset _IDEASY + return ${return_code} + fi fi ide_env= if [ "${OSTYPE}" = "cygwin" ] || [ "${OSTYPE}" = "msys" ]; then - ide_env="$(ideasy env --bash)" + ide_env="$("${_IDEASY}" env --bash)" else - ide_env="$(ideasy env)" + ide_env="$("${_IDEASY}" env)" fi -eval "$ide_env" +if [ $? = 0 ]; then + eval "${ide_env}" + if [ $# = 0 ]; then + echo "IDE environment variables have been set for ${IDE_HOME} in workspace ${WORKSPACE}" + fi +fi +unset _IDEASY unset ide_env +unset return_code diff --git a/cli/src/test/java/com/devonfw/tools/ide/cli/CliArgumentTest.java b/cli/src/test/java/com/devonfw/tools/ide/cli/CliArgumentTest.java index 2e62ea1d6..cf523951d 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/cli/CliArgumentTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/cli/CliArgumentTest.java @@ -1,151 +1,155 @@ -package com.devonfw.tools.ide.cli; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -/** - * Test of {@link CliArgument}. - */ -public class CliArgumentTest extends Assertions { - - private CliArgument of(String... args) { - - return of(false, args); - } - - private CliArgument of(boolean split, String... args) { - - CliArgument arg = CliArgument.of(args); - assertThat(arg.get()).isEqualTo(CliArgument.NAME_START); - assertThat(arg.isStart()).isTrue(); - return arg.getNext(split); - } - - /** - * Test of {@link CliArgument} with simple usage. - */ - @Test - public void testSimple() { - - // arrange - String[] args = { "one", "two", "three" }; - // act - CliArgument arg = of(args); - // assert - assertThat(arg.get()).isEqualTo("one"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(true); - assertThat(arg.get()).isEqualTo("two"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(true); - assertThat(arg.get()).isEqualTo("three"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(true); - assertThat(arg.isEnd()).isTrue(); - } - - /** - * Test of {@link CliArgument} with combined options. - */ - @Test - public void testCombinedOptions() { - - // arrange - boolean split = true; - String[] args = { "-abc", "-xyz", "--abc", "abc" }; - // act - CliArgument arg = of(split, args); - // assert - assertThat(arg.get()).isEqualTo("-a"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.get()).isEqualTo("-b"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.get()).isEqualTo("-c"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.get()).isEqualTo("-x"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.get()).isEqualTo("-y"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.get()).isEqualTo("-z"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.get()).isEqualTo("--abc"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.get()).isEqualTo("abc"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.isEnd()).isTrue(); - } - - /** - * Test of {@link CliArgument} with combined options. - */ - @Test - public void testCombinedOptionsNoSplit() { - - // arrange - String[] args = { "-abc", "-xyz", "--abc", "abc" }; - // act - CliArgument arg = of(args); - // assert - assertThat(arg.get()).isEqualTo("-abc"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(); - assertThat(arg.get()).isEqualTo("-xyz"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(); - assertThat(arg.get()).isEqualTo("--abc"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(); - assertThat(arg.get()).isEqualTo("abc"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(); - assertThat(arg.isEnd()).isTrue(); - } - - /** - * Test of {@link CliArgument} with key-value arguments. - */ - @Test - public void testKeyValue() { - - // arrange - boolean split = true; - String[] args = { "--locale=de", "time=23:59:59", "key=", "=value", "key==", "==value" }; - // act - CliArgument arg = of(args); - // assert - assertThat(arg.getKey()).isEqualTo("--locale"); - assertThat(arg.getValue()).isEqualTo("de"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.getKey()).isEqualTo("time"); - assertThat(arg.getValue()).isEqualTo("23:59:59"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - // edge-cases - assertThat(arg.getKey()).isEqualTo("key"); - assertThat(arg.getValue()).isEqualTo(""); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.getKey()).isEqualTo(""); - assertThat(arg.getValue()).isEqualTo("value"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.getKey()).isEqualTo("key"); - assertThat(arg.getValue()).isEqualTo("="); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.getKey()).isEqualTo(""); - assertThat(arg.getValue()).isEqualTo("=value"); - assertThat(arg.isEnd()).isFalse(); - arg = arg.getNext(split); - assertThat(arg.isEnd()).isTrue(); - } -} +package com.devonfw.tools.ide.cli; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test of {@link CliArgument}. + */ +public class CliArgumentTest extends Assertions { + + private CliArgument of(String... args) { + + return of(false, args); + } + + private CliArgument of(boolean split, String... args) { + + CliArgument arg = CliArgument.of(args); + assertThat(arg.get()).isEqualTo(CliArgument.NAME_START); + assertThat(arg.isStart()).isTrue(); + return arg.getNext(split); + } + + /** + * Test of {@link CliArgument} with simple usage. + */ + @Test + public void testSimple() { + + // arrange + String[] args = { "one", "two", "three" }; + // act + CliArgument arg = of(args); + // assert + assertThat(arg.get()).isEqualTo("one"); + assertThat(arg.isEnd()).isFalse(); + assertThat(arg.asArray()).isEqualTo(args); + arg = arg.getNext(true); + assertThat(arg.get()).isEqualTo("two"); + assertThat(arg.isEnd()).isFalse(); + assertThat(arg.asArray()).isEqualTo(new String[] { "two", "three" }); + arg = arg.getNext(true); + assertThat(arg.get()).isEqualTo("three"); + assertThat(arg.isEnd()).isFalse(); + assertThat(arg.asArray()).isEqualTo(new String[] { "three" }); + arg = arg.getNext(true); + assertThat(arg.isEnd()).isTrue(); + assertThat(arg.asArray()).isEmpty(); + } + + /** + * Test of {@link CliArgument} with combined options. + */ + @Test + public void testCombinedOptions() { + + // arrange + boolean split = true; + String[] args = { "-abc", "-xyz", "--abc", "abc" }; + // act + CliArgument arg = of(split, args); + // assert + assertThat(arg.get()).isEqualTo("-a"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.get()).isEqualTo("-b"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.get()).isEqualTo("-c"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.get()).isEqualTo("-x"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.get()).isEqualTo("-y"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.get()).isEqualTo("-z"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.get()).isEqualTo("--abc"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.get()).isEqualTo("abc"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.isEnd()).isTrue(); + } + + /** + * Test of {@link CliArgument} with combined options. + */ + @Test + public void testCombinedOptionsNoSplit() { + + // arrange + String[] args = { "-abc", "-xyz", "--abc", "abc" }; + // act + CliArgument arg = of(args); + // assert + assertThat(arg.get()).isEqualTo("-abc"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(); + assertThat(arg.get()).isEqualTo("-xyz"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(); + assertThat(arg.get()).isEqualTo("--abc"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(); + assertThat(arg.get()).isEqualTo("abc"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(); + assertThat(arg.isEnd()).isTrue(); + } + + /** + * Test of {@link CliArgument} with key-value arguments. + */ + @Test + public void testKeyValue() { + + // arrange + boolean split = true; + String[] args = { "--locale=de", "time=23:59:59", "key=", "=value", "key==", "==value" }; + // act + CliArgument arg = of(args); + // assert + assertThat(arg.getKey()).isEqualTo("--locale"); + assertThat(arg.getValue()).isEqualTo("de"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.getKey()).isEqualTo("time"); + assertThat(arg.getValue()).isEqualTo("23:59:59"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + // edge-cases + assertThat(arg.getKey()).isEqualTo("key"); + assertThat(arg.getValue()).isEqualTo(""); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.getKey()).isEqualTo(""); + assertThat(arg.getValue()).isEqualTo("value"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.getKey()).isEqualTo("key"); + assertThat(arg.getValue()).isEqualTo("="); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.getKey()).isEqualTo(""); + assertThat(arg.getValue()).isEqualTo("=value"); + assertThat(arg.isEnd()).isFalse(); + arg = arg.getNext(split); + assertThat(arg.isEnd()).isTrue(); + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java index ecda22e1c..167a62dcb 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java @@ -1,15 +1,19 @@ 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 org.junit.jupiter.api.Test; - -import java.nio.file.Path; +/** + * Test of {@link CreateCommandlet}. + */ class CreateCommandletTest extends AbstractIdeContextTest { - private final String NEW_PROJECT_NAME = "newProject"; + private static final String NEW_PROJECT_NAME = "newProject"; @Test public void testCreateCommandletRun() { diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java index 01df7760f..867b24b6c 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java @@ -1,18 +1,22 @@ package com.devonfw.tools.ide.commandlet; +import java.io.IOException; +import java.nio.file.Files; +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.log.IdeLogLevel; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +/** + * Test of {@link UpdateCommandlet}. + */ class UpdateCommandletTest extends AbstractIdeContextTest { - private final String PROJECT_UPDATE = "update"; + private static final String PROJECT_UPDATE = "update"; @Test public void testRunPullSettingsAndUpdateSoftware() { @@ -28,7 +32,6 @@ public void testRunPullSettingsAndUpdateSoftware() { assertThat(context.getConfPath()).exists(); assertThat(context.getSoftwarePath().resolve("java")).exists(); assertThat(context.getSoftwarePath().resolve("mvn")).exists(); - assertLogMessage(context, IdeLogLevel.SUCCESS, "All 2 steps ended successfully!"); } @Test 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 96d44eb36..74ff877fe 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 @@ -1,5 +1,13 @@ package com.devonfw.tools.ide.context; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Condition; +import org.assertj.core.api.ListAssert; + import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.io.FileAccessImpl; import com.devonfw.tools.ide.io.FileCopyMode; @@ -7,13 +15,6 @@ import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.log.IdeTestLogger; import com.devonfw.tools.ide.repo.ToolRepositoryMock; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.Condition; -import org.assertj.core.api.ListAssert; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; /** * Abstract base class for tests that need mocked instances of {@link IdeContext}. @@ -37,7 +38,7 @@ public abstract class AbstractIdeContextTest extends Assertions { /** * @param testProject the (folder)name of the project test case, in this folder a 'project' folder represents the test - * project in {@link #TEST_PROJECTS}. E.g. "basic". + * project in {@link #TEST_PROJECTS}. E.g. "basic". * @return the {@link IdeTestContext} pointing to that project. */ protected IdeTestContext newContext(String testProject) { @@ -47,7 +48,7 @@ protected IdeTestContext newContext(String testProject) { /** * @param testProject the (folder)name of the project test case, in this folder a 'project' folder represents the test - * project in {@link #TEST_PROJECTS}. E.g. "basic". + * project in {@link #TEST_PROJECTS}. E.g. "basic". * @param projectPath the relative path inside the test project where to create the context. * @return the {@link IdeTestContext} pointing to that project. */ @@ -58,11 +59,11 @@ protected static IdeTestContext newContext(String testProject, String projectPat /** * @param testProject the (folder)name of the project test case, in this folder a 'project' folder represents the test - * project in {@link #TEST_PROJECTS}. E.g. "basic". + * project in {@link #TEST_PROJECTS}. E.g. "basic". * @param projectPath the relative path inside the test project where to create the context. * @param copyForMutation - {@code true} to create a copy of the project that can be modified by the test, - * {@code false} otherwise (only to save resources if you are 100% sure that your test never modifies anything in that - * project.) + * {@code false} otherwise (only to save resources if you are 100% sure that your test never modifies anything + * in that project.) * @return the {@link IdeTestContext} pointing to that project. */ protected static IdeTestContext newContext(String testProject, String projectPath, boolean copyForMutation) { @@ -79,14 +80,13 @@ protected static IdeTestContext newContext(String testProject, String projectPat if (projectPath == null) { projectPath = "project"; } - ToolRepositoryMock mock = null; - Path ideHome = ideRoot.resolve(projectPath); + Path userDir = ideRoot.resolve(projectPath); ToolRepositoryMock toolRepository = null; Path repositoryFolder = ideRoot.resolve("repository"); if (Files.isDirectory(repositoryFolder)) { toolRepository = new ToolRepositoryMock(repositoryFolder); } - IdeTestContext context = new IdeTestContext(ideHome, toolRepository); + IdeTestContext context = new IdeTestContext(userDir, toolRepository); if (toolRepository != null) { toolRepository.setContext(context); } @@ -136,7 +136,7 @@ protected static void assertLogMessage(IdeTestContext context, IdeLogLevel level * @param level the expected {@link IdeLogLevel}. * @param message the expected {@link com.devonfw.tools.ide.log.IdeSubLogger#log(String) log message}. * @param contains - {@code true} if the given {@code message} may only be a sub-string of the log-message to assert, - * {@code false} otherwise (the entire log message including potential parameters being filled in is asserted). + * {@code false} otherwise (the entire log message including potential parameters being filled in is asserted). */ protected static void assertLogMessage(IdeTestContext context, IdeLogLevel level, String message, boolean contains) { diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/context/GitContextMock.java index 7cb12d19d..3b2439a6d 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/GitContextMock.java @@ -2,9 +2,9 @@ import java.nio.file.Path; -import com.devonfw.tools.ide.log.IdeLogLevel; -import com.devonfw.tools.ide.log.IdeSubLogger; - +/** + * Mock implementation of {@link GitContext}. + */ public class GitContextMock implements GitContext { @Override public void pullOrCloneIfNeeded(String repoUrl, String branch, Path targetRepository) { @@ -12,7 +12,7 @@ public void pullOrCloneIfNeeded(String repoUrl, String branch, Path targetReposi } @Override - public void pullOrFetchAndResetIfNeeded(String repoUrl, String branch, Path targetRepository, String remoteName) { + public void pullOrCloneAndResetIfNeeded(String repoUrl, Path targetRepository, String branch, String remoteName) { } @@ -37,7 +37,7 @@ public void pull(Path targetRepository) { } @Override - public void reset(Path targetRepository, String remoteName, String branchName) { + public void reset(Path targetRepository, String branchName, String remoteName) { } @@ -46,9 +46,4 @@ public void cleanup(Path targetRepository) { } - @Override - public IdeSubLogger level(IdeLogLevel level) { - - return null; - } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextProcessContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/context/GitContextProcessContextMock.java index eb257edff..8c1e78ba5 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextProcessContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/GitContextProcessContextMock.java @@ -33,7 +33,7 @@ public class GitContextProcessContextMock implements ProcessContext { * @param errors List of errors. * @param outs List of out texts. * @param exitCode the exit code. - * @param directory + * @param directory the {@link Path} to the git repository. */ public GitContextProcessContextMock(List errors, List outs, int exitCode, Path directory) { @@ -51,7 +51,7 @@ public ProcessContext errorHandling(ProcessErrorHandling handling) { } @Override - public ProcessContext directory(Path directory) { + public ProcessContext directory(Path newDirectory) { return this; } @@ -91,7 +91,7 @@ public ProcessResult run(ProcessMode processMode) { // part of git cleanup checks if a new directory 'new-folder' exists if (this.arguments.contains("ls-files")) { if (Files.exists(this.directory.resolve("new-folder"))) { - outs.add("new-folder"); + this.outs.add("new-folder"); this.exitCode = 0; } } @@ -100,7 +100,7 @@ public ProcessResult run(ProcessMode processMode) { Files.createDirectories(gitFolderPath); Path newFile = Files.createFile(gitFolderPath.resolve("url")); // 3rd argument = repository Url - Files.writeString(newFile, arguments.get(2)); + Files.writeString(newFile, this.arguments.get(2)); this.exitCode = 0; } catch (IOException e) { throw new RuntimeException(e); @@ -134,7 +134,7 @@ public ProcessResult run(ProcessMode processMode) { } } this.arguments.clear(); - return new ProcessResultImpl(exitCode, outs, errors); + return new ProcessResultImpl(this.exitCode, this.outs, this.errors); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTest.java b/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTest.java index 527466dbc..f6cf6003b 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTest.java @@ -16,10 +16,15 @@ import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.io.FileAccessImpl; +/** + * Test of {@link GitContext}. + */ public class GitContextTest extends AbstractIdeContextTest { /** * Runs a git clone in offline mode and expects an exception to be thrown with a message. + * + * @param tempDir a {@link TempDir} {@link Path}. */ @Test public void testRunGitCloneInOfflineModeThrowsException(@TempDir Path tempDir) { @@ -43,6 +48,8 @@ public void testRunGitCloneInOfflineModeThrowsException(@TempDir Path tempDir) { /** * Runs a simulated git clone and checks if a new file with the correct repository URL was created. + * + * @param tempDir a {@link TempDir} {@link Path}. */ @Test public void testRunGitClone(@TempDir Path tempDir) { @@ -62,6 +69,8 @@ public void testRunGitClone(@TempDir Path tempDir) { /** * Runs a simulated git pull without force mode, checks if a new file with the current date was created. + * + * @param tempDir a {@link TempDir} {@link Path}. */ @Test public void testRunGitPullWithoutForce(@TempDir Path tempDir) { @@ -85,6 +94,8 @@ public void testRunGitPullWithoutForce(@TempDir Path tempDir) { /** * Runs a git pull with force mode, creates temporary files to simulate a proper cleanup. + * + * @param tempDir a {@link TempDir} {@link Path}. */ @Test public void testRunGitPullWithForceStartsReset(@TempDir Path tempDir) { @@ -115,13 +126,15 @@ public void testRunGitPullWithForceStartsReset(@TempDir Path tempDir) { IdeContext context = newGitContext(tempDir, errors, outs, 0, true); GitContext gitContext = new GitContextImpl(context); // act - gitContext.pullOrFetchAndResetIfNeeded(gitRepoUrl, "master", tempDir, "origin"); + gitContext.pullOrCloneAndResetIfNeeded(gitRepoUrl, tempDir, "master", "origin"); // assert assertThat(modifiedFile).hasContent("original"); } /** * Runs a git pull with force and starts a cleanup (checks if an untracked folder was removed). + * + * @param tempDir a {@link TempDir} {@link Path}. */ @Test public void testRunGitPullWithForceStartsCleanup(@TempDir Path tempDir) { @@ -143,7 +156,7 @@ public void testRunGitPullWithForceStartsCleanup(@TempDir Path tempDir) { throw new RuntimeException(e); } // act - gitContext.pullOrFetchAndResetIfNeeded(gitRepoUrl, "master", tempDir, "origin"); + gitContext.pullOrCloneAndResetIfNeeded(gitRepoUrl, tempDir, "master", "origin"); // assert assertThat(tempDir.resolve("new-folder")).doesNotExist(); } diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTestContext.java b/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTestContext.java index f214d5007..89f7a5cb6 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTestContext.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTestContext.java @@ -10,7 +10,7 @@ import com.devonfw.tools.ide.process.ProcessContext; /** - * Implementation of {@link IdeContext} for testing. + * Implementation of {@link IdeContext} for testing {@link GitContext}. */ public class GitContextTestContext extends AbstractIdeTestContext { @@ -20,8 +20,6 @@ public class GitContextTestContext extends AbstractIdeTestContext { private int exitCode; - private Path directory; - private static boolean testOnlineMode; /** @@ -38,7 +36,6 @@ public GitContextTestContext(boolean isOnline, Path userDir, String... answers) this.errors = new ArrayList<>(); this.outs = new ArrayList<>(); this.exitCode = 0; - this.directory = userDir; } @Override @@ -64,27 +61,31 @@ public static GitContextTestContext of() { @Override public ProcessContext newProcess() { - return new GitContextProcessContextMock(this.errors, this.outs, this.exitCode, this.directory); + return new GitContextProcessContextMock(this.errors, this.outs, this.exitCode, getCwd()); } + /** + * @param errors the {@link List} of errors (stderr lines) of the mocked git process. + */ public void setErrors(List errors) { this.errors = errors; } + /** + * @param outs the {@link List} of outputs (stdout lines) of the mocked git process. + */ public void setOuts(List outs) { this.outs = outs; } + /** + * @param exitCode the return code of the mocked git process. + */ public void setExitCode(int exitCode) { this.exitCode = exitCode; } - public void setDirectory(Path directory) { - - this.directory = directory; - } - } diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/ProcessContextTestImpl.java b/cli/src/test/java/com/devonfw/tools/ide/context/ProcessContextTestImpl.java index 5f53d5069..e2467066f 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/ProcessContextTestImpl.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/ProcessContextTestImpl.java @@ -4,8 +4,16 @@ import com.devonfw.tools.ide.process.ProcessMode; import com.devonfw.tools.ide.process.ProcessResult; +/** + * Extends {@link ProcessContextImpl} for testing. + */ public class ProcessContextTestImpl extends ProcessContextImpl { + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ public ProcessContextTestImpl(IdeContext context) { super(context); diff --git a/cli/src/test/java/com/devonfw/tools/ide/log/IdeSlf4jLogger.java b/cli/src/test/java/com/devonfw/tools/ide/log/IdeSlf4jLogger.java index f305139b6..5f3176844 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/log/IdeSlf4jLogger.java +++ b/cli/src/test/java/com/devonfw/tools/ide/log/IdeSlf4jLogger.java @@ -1,50 +1,71 @@ -package com.devonfw.tools.ide.log; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.event.Level; - -/** - * Implementation of {@link IdeSubLogger} for testing that delegates to slf4j. - */ -public class IdeSlf4jLogger extends AbstractIdeSubLogger { - - private static final Logger LOG = LoggerFactory.getLogger(IdeSlf4jLogger.class); - - private final Level logLevel; - - /** - * The constructor. - * - * @param level the {@link #getLevel() log-level}. - */ - public IdeSlf4jLogger(IdeLogLevel level) { - - super(level); - this.logLevel = switch (level) { - case TRACE -> Level.TRACE; - case DEBUG -> Level.DEBUG; - case INFO, STEP, INTERACTION, SUCCESS -> Level.INFO; - case WARNING -> Level.WARN; - case ERROR -> Level.ERROR; - default -> throw new IllegalArgumentException("" + level); - }; - } - - @Override - public void log(String message) { - - if ((this.level == IdeLogLevel.STEP) || (this.level == IdeLogLevel.INTERACTION) - || (this.level == IdeLogLevel.SUCCESS)) { - message = this.level.name() + ":" + message; - } - LOG.atLevel(this.logLevel).log(message); - } - - @Override - public boolean isEnabled() { - - return LOG.isEnabledForLevel(this.logLevel); - } - -} +package com.devonfw.tools.ide.log; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.slf4j.spi.LoggingEventBuilder; + +/** + * Implementation of {@link IdeSubLogger} for testing that delegates to slf4j. + */ +public class IdeSlf4jLogger extends AbstractIdeSubLogger { + + private static final Logger LOG = LoggerFactory.getLogger(IdeSlf4jLogger.class); + + private final Level logLevel; + + /** + * The constructor. + * + * @param level the {@link #getLevel() log-level}. + */ + public IdeSlf4jLogger(IdeLogLevel level) { + + super(level); + this.logLevel = switch (level) { + case TRACE -> Level.TRACE; + case DEBUG -> Level.DEBUG; + case INFO, STEP, INTERACTION, SUCCESS -> Level.INFO; + case WARNING -> Level.WARN; + case ERROR -> Level.ERROR; + default -> throw new IllegalArgumentException("" + level); + }; + } + + @Override + public String log(Throwable error, String message, Object... args) { + + String msg = message; + if ((this.level == IdeLogLevel.STEP) || (this.level == IdeLogLevel.INTERACTION) + || (this.level == IdeLogLevel.SUCCESS)) { + msg = this.level.name() + ":" + message; + } + LoggingEventBuilder builder = LOG.atLevel(this.logLevel); + if (error != null) { + builder.setCause(error); + } + if (args == null) { + builder.log(msg); + } else { + builder.log(msg, args); + } + if (message == null) { + if (error == null) { + return null; + } else { + return error.toString(); + } + } else if (args == null) { + return message; + } else { + return compose(message, args); + } + } + + @Override + public boolean isEnabled() { + + return LOG.isEnabledForLevel(this.logLevel); + } + +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/log/IdeTestLogger.java b/cli/src/test/java/com/devonfw/tools/ide/log/IdeTestLogger.java index 5b6eb7cc9..4faa35949 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/log/IdeTestLogger.java +++ b/cli/src/test/java/com/devonfw/tools/ide/log/IdeTestLogger.java @@ -23,10 +23,11 @@ public IdeTestLogger(IdeLogLevel level) { } @Override - public void log(String message) { + public String log(Throwable error, String message, Object... args) { - super.log(message); - this.messages.add(message); + String result = super.log(error, message, args); + this.messages.add(result); + return result; } /** diff --git a/cli/src/test/java/com/devonfw/tools/ide/variable/IdeVariablesTest.java b/cli/src/test/java/com/devonfw/tools/ide/variable/IdeVariablesTest.java new file mode 100644 index 000000000..ff263d635 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/variable/IdeVariablesTest.java @@ -0,0 +1,28 @@ +package com.devonfw.tools.ide.variable; + +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.context.IdeTestContextMock; + +/** + * Test of {@link IdeVariables}. + */ +public class IdeVariablesTest extends Assertions { + + /** Test of {@link IdeVariables#IDE_TOOLS}. */ + @Test + public void testIdeTools() { + + // arrange + IdeContext context = IdeTestContextMock.get(); + // act + List ideTools = IdeVariables.IDE_TOOLS.get(context); + // assert + assertThat(ideTools).containsExactly("mvn", "npm"); + } + +}