diff --git a/cli/pom.xml b/cli/pom.xml index ed62690d1..384dd94ab 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -17,6 +17,8 @@ ${project.artifactId}-${os.detected.classifier}-${os.detected.arch} 17 0.9.28 + 3.24.1 + 2.4.0 @@ -75,6 +77,16 @@ progressbar 0.10.0 + + org.jline + jline + ${jline.version} + + + org.fusesource.jansi + jansi + ${jansi.version} + 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 4cbd6f521..9e18715d6 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 @@ -7,10 +7,10 @@ import java.util.Map; import java.util.Objects; -import com.devonfw.tools.ide.completion.CompletionCandidateCollector; 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; /** @@ -208,12 +208,11 @@ public String toString() { } /** - * @param version the {@link VersionIdentifier} to complete. - * @param collector the {@link CompletionCandidateCollector}. - * @return {@code true} on success, {@code false} otherwise. + * @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 boolean completeVersion(VersionIdentifier version, CompletionCandidateCollector collector) { + public ToolCommandlet getToolForVersionCompletion() { - return false; + return null; } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index 838b0fc67..15c5cdbe0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -9,12 +9,16 @@ 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.aws.Aws; import com.devonfw.tools.ide.tool.az.Azure; +import com.devonfw.tools.ide.tool.cobigen.Cobigen; import com.devonfw.tools.ide.tool.eclipse.Eclipse; +import com.devonfw.tools.ide.tool.gcviewer.GcViewer; import com.devonfw.tools.ide.tool.gh.Gh; import com.devonfw.tools.ide.tool.gradle.Gradle; import com.devonfw.tools.ide.tool.helm.Helm; import com.devonfw.tools.ide.tool.java.Java; +import com.devonfw.tools.ide.tool.jmc.Jmc; import com.devonfw.tools.ide.tool.kotlinc.Kotlinc; import com.devonfw.tools.ide.tool.kotlinc.KotlincNative; import com.devonfw.tools.ide.tool.mvn.Mvn; @@ -52,16 +56,21 @@ public CommandletManagerImpl(IdeContext context) { add(new HelpCommandlet(context)); add(new EnvironmentCommandlet(context)); add(new CompleteCommandlet(context)); + add(new ShellCommandlet(context)); add(new InstallCommandlet(context)); add(new VersionSetCommandlet(context)); add(new VersionGetCommandlet(context)); add(new VersionListCommandlet(context)); + add(new EditionGetCommandlet(context)); + add(new EditionSetCommandlet(context)); + add(new EditionListCommandlet(context)); add(new VersionCommandlet(context)); add(new Gh(context)); add(new Helm(context)); add(new Java(context)); add(new Node(context)); add(new Mvn(context)); + add(new GcViewer(context)); add(new Gradle(context)); add(new Eclipse(context)); add(new Terraform(context)); @@ -71,6 +80,9 @@ public CommandletManagerImpl(IdeContext context) { add(new KotlincNative(context)); add(new Vscode(context)); add(new Azure(context)); + add(new Aws(context)); + add(new Cobigen(context)); + add(new Jmc(context)); } private void add(Commandlet commandlet) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CompleteCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CompleteCommandlet.java index e3708b052..a9d856a98 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CompleteCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CompleteCommandlet.java @@ -1,6 +1,5 @@ package com.devonfw.tools.ide.commandlet; -import java.util.Collections; import java.util.List; import com.devonfw.tools.ide.cli.CliArguments; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/EditionGetCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EditionGetCommandlet.java new file mode 100644 index 000000000..23c507377 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EditionGetCommandlet.java @@ -0,0 +1,50 @@ +package com.devonfw.tools.ide.commandlet; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.property.ToolProperty; +import com.devonfw.tools.ide.tool.ToolCommandlet; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * An internal {@link Commandlet} to get the installed edition for a tool. + * + * @see ToolCommandlet#getInstalledEdition() + */ +public class EditionGetCommandlet extends Commandlet { + + /** The tool to get the edition of. */ + public final ToolProperty tool; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public EditionGetCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + this.tool = add(new ToolProperty("", true, "tool")); + } + + @Override + public String getName() { + + return "get-edition"; + } + + @Override + public void run() { + + ToolCommandlet commandlet = this.tool.getValue(); + VersionIdentifier installedVersion = commandlet.getInstalledVersion(); + if (installedVersion == null) { + this.context.info("The configured edition for tool {} is {}", commandlet.getName(), commandlet.getEdition()); + this.context.info("To install that edition call the following command:"); + this.context.info("ide install {}", commandlet.getName()); + return; + } + String installedEdition = commandlet.getInstalledEdition(); + this.context.info(installedEdition); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/EditionListCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EditionListCommandlet.java new file mode 100644 index 000000000..eea3a261b --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EditionListCommandlet.java @@ -0,0 +1,42 @@ +package com.devonfw.tools.ide.commandlet; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.property.ToolProperty; +import com.devonfw.tools.ide.tool.ToolCommandlet; + +/** + * An internal {@link Commandlet} to list editions for a tool. + * + * @see ToolCommandlet#listEditions() + */ +public class EditionListCommandlet extends Commandlet { + + /** The tool to list the editions of. */ + public final ToolProperty tool; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public EditionListCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + this.tool = add(new ToolProperty("", true, "tool")); + } + + @Override + public String getName() { + + return "list-editions"; + } + + @Override + public void run() { + + ToolCommandlet commandlet = this.tool.getValue(); + commandlet.listEditions(); + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/EditionSetCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EditionSetCommandlet.java new file mode 100644 index 000000000..ee851d040 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EditionSetCommandlet.java @@ -0,0 +1,47 @@ +package com.devonfw.tools.ide.commandlet; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.property.EditionProperty; +import com.devonfw.tools.ide.property.ToolProperty; +import com.devonfw.tools.ide.tool.ToolCommandlet; + +/** + * An internal {@link Commandlet} to set a tool edition. + */ +public class EditionSetCommandlet extends Commandlet { + + /** The tool to set the edition of. */ + public final ToolProperty tool; + + /** The edition to set. */ + public final EditionProperty edition; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public EditionSetCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + this.tool = add(new ToolProperty("", true, "tool")); + this.edition = add(new EditionProperty("", true, "edition")); + } + + @Override + public String getName() { + + return "set-edition"; + } + + @Override + public void run() { + + ToolCommandlet commandlet = this.tool.getValue(); + String edition = this.edition.getValue(); + + commandlet.setEdition(edition); + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/InstallCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/InstallCommandlet.java index 6396b61a2..696401e09 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/InstallCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/InstallCommandlet.java @@ -49,4 +49,9 @@ public void run() { commandlet.install(false); } + @Override + public ToolCommandlet getToolForVersionCompletion() { + + return this.tool.getValue(); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ShellCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ShellCommandlet.java new file mode 100644 index 000000000..4457ac55d --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ShellCommandlet.java @@ -0,0 +1,205 @@ +package com.devonfw.tools.ide.commandlet; + +import java.io.IOException; +import java.util.Iterator; + +import org.fusesource.jansi.AnsiConsole; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.MaskingCallback; +import org.jline.reader.Parser; +import org.jline.reader.UserInterruptException; +import org.jline.reader.impl.DefaultParser; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.widget.AutosuggestionWidgets; + +import com.devonfw.tools.ide.cli.CliArgument; +import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.completion.IdeCompleter; +import com.devonfw.tools.ide.context.AbstractIdeContext; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.property.BooleanProperty; +import com.devonfw.tools.ide.property.KeywordProperty; +import com.devonfw.tools.ide.property.Property; + +/** + * {@link Commandlet} for internal interactive shell with build-in auto-completion and help. + */ +public final class ShellCommandlet extends Commandlet { + + private static final int AUTOCOMPLETER_MAX_RESULTS = 50; + + private static final int RC_EXIT = 987654321; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public ShellCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + } + + @Override + public String getName() { + + return "shell"; + } + + @Override + public boolean isIdeHomeRequired() { + + return false; + } + + @Override + public void run() { + + try { + // TODO: add BuiltIns here, see: https://github.com/devonfw/IDEasy/issues/168 + + Parser parser = new DefaultParser(); + try (Terminal terminal = TerminalBuilder.builder().build()) { + + // initialize our own completer here + IdeCompleter completer = new IdeCompleter((AbstractIdeContext) this.context); + + LineReader reader = LineReaderBuilder.builder().terminal(terminal).completer(completer).parser(parser) + .variable(LineReader.LIST_MAX, AUTOCOMPLETER_MAX_RESULTS).build(); + + // Create autosuggestion widgets + AutosuggestionWidgets autosuggestionWidgets = new AutosuggestionWidgets(reader); + // Enable autosuggestions + autosuggestionWidgets.enable(); + + // TODO: implement TailTipWidgets, see: https://github.com/devonfw/IDEasy/issues/169 + + String prompt = "ide> "; + String rightPrompt = null; + String line; + + AnsiConsole.systemInstall(); + while (true) { + try { + line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null); + reader.getHistory().add(line); + int rc = runCommand(line); + if (rc == RC_EXIT) { + return; + } + } catch (UserInterruptException e) { + // Ignore CTRL+C + return; + } catch (EndOfFileException e) { + // CTRL+D + return; + } finally { + AnsiConsole.systemUninstall(); + } + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + } catch (Exception e) { + throw new RuntimeException("Unexpected error during interactive auto-completion", e); + } + } + + /** + * Converts String of arguments to array and runs the command + * + * @param args String of arguments + * @return status code + */ + private int runCommand(String args) { + + if ("exit".equals(args) || "quit".equals(args)) { + return RC_EXIT; + } + String[] arguments = args.split(" ", 0); + CliArguments cliArgs = new CliArguments(arguments); + cliArgs.next(); + return ((AbstractIdeContext) this.context).run(cliArgs); + } + + /** + * @param argument the current {@link CliArgument} (position) to match. + * @param commandlet the potential {@link Commandlet} to match. + * @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). + */ + private boolean apply(CliArgument argument, Commandlet commandlet) { + + this.context.trace("Trying to match arguments to commandlet {}", commandlet.getName()); + CliArgument currentArgument = argument; + Iterator> valueIterator = commandlet.getValues().iterator(); + Property currentProperty = null; + boolean endOpts = false; + while (!currentArgument.isEnd()) { + if (currentArgument.isEndOptions()) { + endOpts = true; + } else { + String arg = currentArgument.get(); + this.context.trace("Trying to match argument '{}'", currentArgument); + if ((currentProperty != null) && (currentProperty.isExpectValue())) { + currentProperty.setValueAsString(arg, this.context); + if (!currentProperty.isMultiValued()) { + currentProperty = null; + } + } else { + Property property = null; + if (!endOpts) { + property = commandlet.getOption(currentArgument.getKey()); + } + if (property == null) { + if (!valueIterator.hasNext()) { + this.context.trace("No option or next value found"); + return false; + } + currentProperty = valueIterator.next(); + this.context.trace("Next value candidate is {}", currentProperty); + if (currentProperty instanceof KeywordProperty keyword) { + if (keyword.matches(arg)) { + keyword.setValue(Boolean.TRUE); + this.context.trace("Keyword matched"); + } else { + this.context.trace("Missing keyword"); + return false; + } + } else { + boolean success = currentProperty.assignValueAsString(arg, this.context, commandlet); + if (!success && currentProperty.isRequired()) { + return false; + } + } + if ((currentProperty != null) && !currentProperty.isMultiValued()) { + currentProperty = null; + } + } else { + this.context.trace("Found option by name"); + String value = currentArgument.getValue(); + if (value != null) { + property.setValueAsString(value, this.context); + } else if (property instanceof BooleanProperty) { + ((BooleanProperty) property).setValue(Boolean.TRUE); + } else { + currentProperty = property; + if (property.isEndOptions()) { + endOpts = true; + } + throw new UnsupportedOperationException("not implemented"); + } + } + } + } + currentArgument = currentArgument.getNext(!endOpts); + } + return commandlet.validate(); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionGetCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionGetCommandlet.java index 153d93c7d..c7042d02f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionGetCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionGetCommandlet.java @@ -8,9 +8,9 @@ import com.devonfw.tools.ide.version.VersionIdentifier; /** - * An internal {@link Commandlet} to set a tool version. + * An internal {@link Commandlet} to get the installed version for a tool. * - * @see ToolCommandlet#setVersion(VersionIdentifier, boolean) + * @see ToolCommandlet#getInstalledVersion() */ public class VersionGetCommandlet extends Commandlet { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionListCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionListCommandlet.java index 16d8b7d50..1b4ab40ec 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionListCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionListCommandlet.java @@ -3,12 +3,11 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.property.ToolProperty; import com.devonfw.tools.ide.tool.ToolCommandlet; -import com.devonfw.tools.ide.version.VersionIdentifier; /** - * An internal {@link Commandlet} to set a tool version. + * An internal {@link Commandlet} to list versions for a tool. * - * @see ToolCommandlet#setVersion(VersionIdentifier, boolean) + * @see ToolCommandlet#listVersions() */ public class VersionListCommandlet extends Commandlet { @@ -30,7 +29,7 @@ public VersionListCommandlet(IdeContext context) { @Override public String getName() { - return "list-version"; + return "list-versions"; } @Override diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionSetCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionSetCommandlet.java index 9c309360a..1bba9e31e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionSetCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/VersionSetCommandlet.java @@ -1,15 +1,10 @@ package com.devonfw.tools.ide.commandlet; -import java.util.List; -import java.util.stream.IntStream; - -import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.property.ToolProperty; import com.devonfw.tools.ide.property.VersionProperty; import com.devonfw.tools.ide.tool.ToolCommandlet; import com.devonfw.tools.ide.version.VersionIdentifier; -import com.devonfw.tools.ide.version.VersionSegment; /** * An internal {@link Commandlet} to set a tool version. @@ -52,31 +47,9 @@ public void run() { } @Override - public boolean completeVersion(VersionIdentifier version2complete, CompletionCandidateCollector collector) { + public ToolCommandlet getToolForVersionCompletion() { - ToolCommandlet toolCmd = this.tool.getValue(); - if (toolCmd != null) { - String text; - if (version2complete == null) { - text = ""; - } else { - text = version2complete.toString(); - if (version2complete.isPattern()) { - collector.add(text, this.version, this); - return true; - } - } - collector.add(text + VersionSegment.PATTERN_MATCH_ANY_STABLE_VERSION, this.tool, this); - collector.add(text + VersionSegment.PATTERN_MATCH_ANY_VERSION, this.tool, this); - List versions = this.context.getUrls().getSortedVersions(toolCmd.getName(), - toolCmd.getEdition()); - int size = versions.size(); - String[] sorderCandidates = IntStream.rangeClosed(1, size).mapToObj(i -> versions.get(size - i).toString()) - .toArray(s -> new String[s]); - collector.addAllMatches(text, sorderCandidates, this.version, this); - return true; - } - return super.completeVersion(version2complete, collector); + return this.tool.getValue(); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java b/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java index 85bac342c..f293f78d2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java +++ b/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -26,7 +25,7 @@ public class SystemPath { private final Map tool2pathMap; private final List paths; - + private final IdeContext context; private static final List EXTENSION_PRIORITY = List.of(".exe", ".cmd", ".bat", ".msi", ".ps1", ""); @@ -61,7 +60,7 @@ public SystemPath(String envPath, Path softwarePath, char pathSeparator, IdeCont this.paths = new ArrayList<>(); String[] envPaths = envPath.split(Character.toString(pathSeparator)); for (String segment : envPaths) { - Path path = Paths.get(segment); + Path path = Path.of(segment); String tool = getTool(path, softwarePath); if (tool == null) { this.paths.add(path); @@ -129,7 +128,13 @@ private Path findBinaryInOrder(Path path, String tool) { return null; } + /** + * @param toolPath the {@link Path} to the tool installation. + * @return the {@link Path} to the binary executable of the tool. E.g. is "software/mvn" is given + * "software/mvn/bin/mvn" could be returned. + */ public Path findBinary(Path toolPath) { + Path parent = toolPath.getParent(); String fileName = toolPath.getFileName().toString(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidate.java b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidate.java index 12ca83369..c62bd07d8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidate.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidate.java @@ -4,12 +4,13 @@ * Candidate for auto-completion. * * @param text the text to suggest (CLI argument value). + * @param description the description of the candidate. */ -public record CompletionCandidate(String text /* , String description */) implements Comparable { +public record CompletionCandidate(String text, String description) implements Comparable { @Override public int compareTo(CompletionCandidate o) { - return text.compareTo(o.text); + return this.text.compareTo(o.text); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollector.java b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollector.java index 97de8457a..9d1c64f1f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollector.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollector.java @@ -1,11 +1,9 @@ package com.devonfw.tools.ide.completion; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import com.devonfw.tools.ide.commandlet.Commandlet; -import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.property.Property; /** @@ -15,10 +13,27 @@ public interface CompletionCandidateCollector { /** * @param text the suggested word to add to auto-completion. + * @param description the description of the suggestion candidate or {@code null} to determine automatically form the given parameters. * @param property the {@link Property} that triggered this suggestion. * @param commandlet the {@link Commandlet} owning the {@link Property}. */ - void add(String text, Property property, Commandlet commandlet); + void add(String text, String description, Property property, Commandlet commandlet); + + /** + * @param text the suggested word to add to auto-completion. + * @param description the description of the suggestion candidate or {@code null} to determine automatically form the given parameters. + * @param property the {@link Property} that triggered this suggestion. + * @param commandlet the {@link Commandlet} owning the {@link Property}. + * @return the {@link CompletionCandidate} for the given parameters. + */ + default CompletionCandidate createCandidate(String text, String description, Property property, Commandlet commandlet) { + + if (description == null) { + // compute description from property + commandlet like in HelpCommandlet? + } + CompletionCandidate candidate = new CompletionCandidate(text, description); + return candidate; + } /** * @param text the suggested word to add to auto-completion. @@ -31,14 +46,14 @@ default int addAllMatches(String text, String[] sortedCandidates, Property pr if (text.isEmpty()) { for (String candidate : sortedCandidates) { - add(candidate, property, commandlet); + add(candidate, "", property, commandlet); } return sortedCandidates.length; } int count = 0; int index = Arrays.binarySearch(sortedCandidates, text); if (index >= 0) { - add(sortedCandidates[index], property, commandlet); + add(sortedCandidates[index], "", property, commandlet); index++; count++; } else { @@ -46,7 +61,7 @@ default int addAllMatches(String text, String[] sortedCandidates, Property pr } while ((index >= 0) && (index < sortedCandidates.length)) { if (sortedCandidates[index].startsWith(text)) { - add(sortedCandidates[index], property, commandlet); + add(sortedCandidates[index], "", property, commandlet); count++; } else { break; @@ -69,4 +84,14 @@ default void clear() { */ List getCandidates(); + /** + * Disables the {@link #getSortedCandidates() sorting}. + */ + void disableSorting(); + + /** + * @return the sorted {@link #getCandidates() candidates}. + */ + List getSortedCandidates(); + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorAdapter.java b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorAdapter.java index 2b3c1fd7e..16fb62333 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorAdapter.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorAdapter.java @@ -29,9 +29,9 @@ public CompletionCandidateCollectorAdapter(String prefix, CompletionCandidateCol } @Override - public void add(String text, Property property, Commandlet commandlet) { + public void add(String text, String description, Property property, Commandlet commandlet) { - this.delegate.add(this.prefix + text, property, commandlet); + this.delegate.add(this.prefix + text, description, property, commandlet); } @Override @@ -39,4 +39,16 @@ public List getCandidates() { return this.delegate.getCandidates(); } + + @Override + public List getSortedCandidates() { + + return this.delegate.getSortedCandidates(); + } + + @Override + public void disableSorting() { + + this.delegate.disableSorting(); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorDefault.java b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorDefault.java index f4ba507ba..41a7a1102 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorDefault.java +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/CompletionCandidateCollectorDefault.java @@ -1,7 +1,7 @@ package com.devonfw.tools.ide.completion; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import com.devonfw.tools.ide.commandlet.Commandlet; @@ -17,6 +17,8 @@ public class CompletionCandidateCollectorDefault implements CompletionCandidateC private final IdeContext context; + private boolean sortCandidates; + /** * The constructor. * @@ -27,12 +29,13 @@ public CompletionCandidateCollectorDefault(IdeContext context) { super(); this.candidates = new ArrayList<>(); this.context = context; + this.sortCandidates = true; } @Override - public void add(String text, Property property, Commandlet commandlet) { + public void add(String text, String description, Property property, Commandlet commandlet) { - CompletionCandidate candidate = new CompletionCandidate(text); + CompletionCandidate candidate = createCandidate(text, description, property, commandlet); this.candidates.add(candidate); this.context.trace("Added {} for auto-completion of property {}.{}", candidate, commandlet, property); } @@ -43,6 +46,21 @@ public List getCandidates() { return this.candidates; } + @Override + public List getSortedCandidates() { + + if (this.sortCandidates) { + Collections.sort(this.candidates); + } + return this.candidates; + } + + @Override + public void disableSorting() { + + this.sortCandidates = false; + } + @Override public String toString() { diff --git a/cli/src/main/java/com/devonfw/tools/ide/completion/IdeCompleter.java b/cli/src/main/java/com/devonfw/tools/ide/completion/IdeCompleter.java new file mode 100644 index 000000000..4f2edeb8f --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/completion/IdeCompleter.java @@ -0,0 +1,42 @@ +package com.devonfw.tools.ide.completion; + +import java.util.List; + +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.context.AbstractIdeContext; + +/** + * Implements the {@link Completer} for jline3 autocompletion. Inspired by picocli + */ +public class IdeCompleter implements Completer { + + private final AbstractIdeContext context; + + /** + * The constructor. + * + * @param context the {@link AbstractIdeContext}. + */ + public IdeCompleter(AbstractIdeContext context) { + + super(); + this.context = context; + } + + @Override + public void complete(LineReader reader, ParsedLine commandLine, List candidates) { + List words = commandLine.words(); + CliArguments args = CliArguments.ofCompletion(words.toArray(String[]::new)); + List completion = this.context.complete(args, false); + int i = 0; + for (CompletionCandidate candidate : completion) { + candidates.add(new Candidate(candidate.text(), candidate.text(), null, null, null, null, true, i++)); + } + } + +} 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 2cd44eb3f..9cb1533a3 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 @@ -4,8 +4,14 @@ import java.net.InetAddress; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; +import java.nio.file.attribute.FileTime; +import java.time.Duration; +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.CliArgument; @@ -114,6 +120,8 @@ public abstract class AbstractIdeContext implements IdeContext { private UrlMetadata urlMetadata; + private static final Duration GIT_PULL_CACHE_DELAY_MILLIS = Duration.ofMillis(30 * 60 * 1000); + /** * The constructor. * @@ -132,7 +140,7 @@ public AbstractIdeContext(IdeLogLevel minLogLevel, Function remotes = result.getOut(); if (remotes.isEmpty()) { String message = "This is a local git repo with no remote - if you did this for testing, you may continue...\n" @@ -612,7 +620,7 @@ public void gitPullOrClone(Path target, String gitRepoUrl) { askToContinue(message); } else { pc.errorHandling(ProcessErrorHandling.WARNING); - result = pc.addArg("pull").run(false); + result = pc.addArg("pull").run(false, false); if (!result.isSuccessful()) { String message = "Failed to update git repository at " + target; if (this.offlineMode) { @@ -653,6 +661,45 @@ public void gitPullOrClone(Path target, String gitRepoUrl) { } } + /** + * Checks if the Git repository in the specified target folder needs an update by inspecting the modification time of + * a magic file. + * + * @param urlsPath The Path to the Urls repository. + * @param repoUrl The git remote URL of the Urls repository. + */ + + private void gitPullOrCloneIfNeeded(Path urlsPath, String repoUrl) { + + Path gitDirectory = urlsPath.resolve(".git"); + + // Check if the .git directory exists + if (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()) || isForceMode()) { + gitPullOrClone(urlsPath, repoUrl); + try { + Files.setLastModifiedTime(magicFilePath, FileTime.fromMillis(currentTime)); + } catch (IOException e) { + throw new IllegalStateException("Could not read or write in " + magicFilePath, e); + } + } + } else { + // If the .git directory does not exist, perform git clone + gitPullOrClone(urlsPath, repoUrl); + } + } + @Override public IdeSubLogger level(IdeLogLevel level) { @@ -765,8 +812,9 @@ public int run(CliArguments arguments) { } /** - * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet, CompletionCandidateCollector) apply} and - * {@link Commandlet#run() run}. + * @param cmd the potential {@link Commandlet} to + * {@link #apply(CliArguments, Commandlet, CompletionCandidateCollector) apply} and {@link Commandlet#run() + * run}. * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, * {@code false} otherwise (the {@link Commandlet} did not match and we have to try a different candidate). */ @@ -814,7 +862,7 @@ public List complete(CliArguments arguments, boolean includ if (firstCandidate != null) { matches = apply(arguments.copy(), firstCandidate, collector); } else if (current.isCombinedShortOption()) { - collector.add(keyword, null, null); + collector.add(keyword, null, null, null); } if (!matches) { for (Commandlet cmd : this.commandletManager.getCommandlets()) { @@ -824,9 +872,7 @@ public List complete(CliArguments arguments, boolean includ } } } - List candidates = collector.getCandidates(); - Collections.sort(candidates); - return candidates; + return collector.getSortedCandidates(); } /** 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 0e3844103..53a9c5d84 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 @@ -1,7 +1,5 @@ package com.devonfw.tools.ide.context; -import me.tongfei.progressbar.ProgressBarBuilder; -import me.tongfei.progressbar.ProgressBarStyle; import java.util.Scanner; import com.devonfw.tools.ide.io.IdeProgressBar; @@ -9,6 +7,9 @@ import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.log.IdeSubLoggerOut; +import me.tongfei.progressbar.ProgressBarBuilder; +import me.tongfei.progressbar.ProgressBarStyle; + /** * Default implementation of {@link IdeContext} using the console. */ 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 d99e67d1f..bf6be144c 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,7 +1,6 @@ package com.devonfw.tools.ide.environment; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collection; import java.util.Locale; @@ -46,7 +45,7 @@ default Path getPath(String name) { if (value == null) { return null; } - return Paths.get(value); + return Path.of(value); } /** @@ -226,4 +225,13 @@ 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/io/FileAccess.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java index d20361128..5d105d8e0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java @@ -62,7 +62,7 @@ public interface FileAccess { * Creates a symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows * junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, which must * point to absolute paths. Therefore, the created link will be absolute instead of relative. - * + * * @param source the source {@link Path} to link to, may be relative or absolute. * @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}. * @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute. @@ -73,7 +73,7 @@ public interface FileAccess { * Creates a relative symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a * Windows junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, * which must point to absolute paths. Therefore, the created link will be absolute instead of relative. - * + * * @param source the source {@link Path} to link to, may be relative or absolute. * @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java index 08985d6d5..54c791293 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java @@ -16,8 +16,9 @@ import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -25,12 +26,14 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; @@ -78,7 +81,7 @@ public void download(String url, Path target) { } else if (url.startsWith("ftp") || url.startsWith("sftp")) { throw new IllegalArgumentException("Unsupported download URL: " + url); } else { - Path source = Paths.get(url); + Path source = Path.of(url); if (isFile(source)) { // network drive copyFileWithProgressBar(source, target); @@ -315,8 +318,7 @@ private void deleteLinkIfExists(Path path) throws IOException { return; } } - exists = exists || Files.exists(path); // "||" since broken junctions are not detected by - // Files.exists(brokenJunction) + exists = exists || Files.exists(path); boolean isSymlink = exists && Files.isSymbolicLink(path); assert !(isSymlink && isJunction); @@ -355,7 +357,7 @@ private Path adaptPath(Path source, Path targetLink, boolean relative) throws IO if (relative) { source = targetLink.getParent().relativize(source); // to make relative links like this work: dir/link -> dir - source = (source.toString().isEmpty()) ? Paths.get(".") : source; + source = (source.toString().isEmpty()) ? Path.of(".") : source; } } else { // source is relative if (relative) { @@ -363,7 +365,7 @@ private Path adaptPath(Path source, Path targetLink, boolean relative) throws IO // this ../d1/../d2 to ../d2 source = targetLink.getParent() .relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS)); - source = (source.toString().isEmpty()) ? Paths.get(".") : source; + source = (source.toString().isEmpty()) ? Path.of(".") : source; } else { // !relative try { source = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS); @@ -378,7 +380,7 @@ private Path adaptPath(Path source, Path targetLink, boolean relative) throws IO /** * Creates a Windows junction at {@code targetLink} pointing to {@code source}. - * + * * @param source must be another Windows junction or a directory. * @param targetLink the location of the Windows junction. */ @@ -495,13 +497,41 @@ public void untar(Path file, Path targetDir, TarCompression compression) { unpack(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in))); } + /** + * @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file + * permissions of a file on a Unix file system. + * @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--" + */ + public static String generatePermissionString(int permissions) { + + // Ensure that only the last 9 bits are considered + permissions &= 0b111111111; + + StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx"); + + for (int i = 0; i < 9; i++) { + int mask = 1 << i; + char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-'; + permissionStringBuilder.setCharAt(8 - i, currentChar); + } + + return permissionStringBuilder.toString(); + } + private void unpack(Path file, Path targetDir, Function unpacker) { this.context.trace("Unpacking archive {} to {}", file, targetDir); try (InputStream is = Files.newInputStream(file); ArchiveInputStream ais = unpacker.apply(is)) { ArchiveEntry entry = ais.getNextEntry(); + boolean isTar = ais instanceof TarArchiveInputStream; while (entry != null) { - Path entryName = Paths.get(entry.getName()); + String permissionStr = null; + if (isTar) { + int tarMode = ((TarArchiveEntry) entry).getMode(); + permissionStr = generatePermissionString(tarMode); + } + + Path entryName = Path.of(entry.getName()); Path entryPath = targetDir.resolve(entryName).toAbsolutePath(); if (!entryPath.startsWith(targetDir)) { throw new IOException("Preventing path traversal attack from " + entryName + " to " + entryPath); @@ -513,6 +543,10 @@ private void unpack(Path file, Path targetDir, Function permissions = PosixFilePermissions.fromString(permissionStr); + Files.setPosixFilePermissions(entryPath, permissions); + } entry = ais.getNextEntry(); } } catch (IOException e) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java index 04e011a9c..32604f399 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java @@ -133,22 +133,16 @@ private JsonValue mergeAndResolve(JsonValue json, JsonValue mergeJson, Environme if (mergeJson == null) { status.updated = true; // JSON to merge does not exist and needs to be created } - switch (json.getValueType()) { - case OBJECT: - return mergeAndResolveObject((JsonObject) json, (JsonObject) mergeJson, variables, status, src); - case ARRAY: - return mergeAndResolveArray((JsonArray) json, (JsonArray) mergeJson, variables, status, src); - case STRING: - return mergeAndResolveString((JsonString) json, (JsonString) mergeJson, variables, status, src); - case NUMBER: - case FALSE: - case TRUE: - case NULL: - return mergeAndResolveNativeType(json, mergeJson, variables, status); - default: + return switch (json.getValueType()) { + case OBJECT -> mergeAndResolveObject((JsonObject) json, (JsonObject) mergeJson, variables, status, src); + case ARRAY -> mergeAndResolveArray((JsonArray) json, (JsonArray) mergeJson, variables, status, src); + case STRING -> mergeAndResolveString((JsonString) json, (JsonString) mergeJson, variables, status, src); + case NUMBER, FALSE, TRUE, NULL -> mergeAndResolveNativeType(json, mergeJson, variables, status); + default -> { this.context.error("Undefined JSON type {}", json.getClass()); - return null; - } + yield null; + } + }; } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/XmlMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/XmlMerger.java index 4be18367c..ae25cf5f9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/XmlMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/XmlMerger.java @@ -182,8 +182,7 @@ private void resolve(Element element, EnvironmentVariables variables, boolean in for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); - if (node instanceof Text) { - Text text = (Text) node; + if (node instanceof Text text) { String value = text.getNodeValue(); String resolvedValue; if (inverse) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/os/WindowsPathSyntax.java b/cli/src/main/java/com/devonfw/tools/ide/os/WindowsPathSyntax.java index 91760184c..92f573e8b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/os/WindowsPathSyntax.java +++ b/cli/src/main/java/com/devonfw/tools/ide/os/WindowsPathSyntax.java @@ -62,16 +62,11 @@ public String replaceDrive(String path, String drive) { throw new IllegalArgumentException(path); } String restPath = path.substring(3); - switch (this) { - case WINDOWS: - restPath = restPath.replace('/', '\\'); - break; - case MSYS: - restPath = restPath.replace('\\', '/'); - break; - default: - throw new IllegalStateException(toString()); - } + restPath = switch (this) { + case WINDOWS -> restPath.replace('/', '\\'); + case MSYS -> restPath.replace('\\', '/'); + default -> throw new IllegalStateException(toString()); + }; return getRootPath(drive) + restPath; } @@ -85,14 +80,11 @@ public String getRootPath(String drive) { if ((drive.length() != 1) || !isLowerLatinLetter(Character.toLowerCase(drive.charAt(0)))) { throw new IllegalArgumentException(drive); } - switch (this) { - case WINDOWS: - return drive.toUpperCase(Locale.ROOT) + ":\\"; - case MSYS: - return "/" + drive.toLowerCase(Locale.ROOT) + "/"; - default: - throw new IllegalStateException(toString()); - } + return switch (this) { + case WINDOWS -> drive.toUpperCase(Locale.ROOT) + ":\\"; + case MSYS -> "/" + drive.toLowerCase(Locale.ROOT) + "/"; + default -> throw new IllegalStateException(toString()); + }; } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContext.java b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContext.java index bcab1cfb9..f2c6cc4a0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContext.java @@ -1,7 +1,6 @@ package com.devonfw.tools.ide.process; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.Objects; @@ -41,7 +40,7 @@ public interface ProcessContext { */ default ProcessContext executable(String executable) { - return executable(Paths.get(executable)); + return executable(Path.of(executable)); } /** @@ -133,7 +132,7 @@ default ProcessContext addArgs(List... args) { */ default int run() { - return run(false).getExitCode(); + return run(false, false).getExitCode(); } /** @@ -146,6 +145,23 @@ default int run() { * and err). * @return the {@link ProcessResult}. */ - ProcessResult run(boolean capture); + default ProcessResult run(boolean capture) { + + return run(capture, false); + } + + /** + * Runs the previously configured {@link #executable(Path) command} with the configured {@link #addArgs(String...) + * arguments}. Will reset the {@link #addArgs(String...) arguments} but not the {@link #executable(Path) command} for + * sub-sequent calls. + * + * @param capture - {@code true} to capture standard {@link ProcessResult#getOut() out} and + * {@link ProcessResult#getErr() err} in the {@link ProcessResult}, {@code false} otherwise (to redirect out + * and err). + * @param runInBackground {@code true}, the process of the command will be run as background process, {@code false} + * otherwise (it will be run as foreground process). + * @return the {@link ProcessResult}. + */ + ProcessResult run(boolean capture, boolean runInBackground); } 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 21f6a9a0e..a3a0ef379 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 @@ -7,7 +7,6 @@ import java.lang.ProcessBuilder.Redirect; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -97,7 +96,12 @@ public ProcessContext withEnvVar(String key, String value) { } @Override - public ProcessResult run(boolean capture) { + public ProcessResult run(boolean capture, boolean isBackgroundProcess) { + + if (isBackgroundProcess) { + this.context + .warning("TODO https://github.com/devonfw/IDEasy/issues/9 Implement background process functionality"); + } if (this.executable == null) { throw new IllegalStateException("Missing executable to run process!"); @@ -220,7 +224,7 @@ private boolean hasSheBang(Path file) { private String findBashOnWindows() { // Check if Git Bash exists in the default location - Path defaultPath = Paths.get("C:\\Program Files\\Git\\bin\\bash.exe"); + Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe"); if (Files.exists(defaultPath)) { return defaultPath.toString(); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/CommandletProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/CommandletProperty.java index f1567ed07..a0854d6bc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/CommandletProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/CommandletProperty.java @@ -49,18 +49,15 @@ protected String format(Commandlet valueToFormat) { } @Override - protected boolean completeValue(String arg, IdeContext context, Commandlet commandlet, + protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) { - boolean matches = false; for (Commandlet cmd : context.getCommandletManager().getCommandlets()) { String cmdName = cmd.getName(); if (cmdName.startsWith(arg)) { - collector.add(cmdName, this, commandlet); - matches = true; + collector.add(cmdName, null, null, cmd); } } - return matches; } @Override diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/EditionProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/EditionProperty.java new file mode 100644 index 000000000..f2226254b --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/property/EditionProperty.java @@ -0,0 +1,45 @@ +package com.devonfw.tools.ide.property; + +import com.devonfw.tools.ide.context.IdeContext; + +import java.util.function.Consumer; + +public class EditionProperty extends Property { + + /** + * The constructor. + * + * @param name the {@link #getName() property name}. + * @param required the {@link #isRequired() required flag}. + * @param alias the {@link #getAlias() property alias}. + */ + public EditionProperty(String name, boolean required, String alias) { + + this(name, required, alias, null); + } + + /** + * The constructor. + * + * @param name the {@link #getName() property name}. + * @param required the {@link #isRequired() required flag}. + * @param alias the {@link #getAlias() property alias}. + * @param validator the {@link Consumer} used to {@link #validate() validate} the {@link #getValue() value}. + */ + public EditionProperty(String name, boolean required, String alias, Consumer validator) { + + super(name, required, alias, validator); + } + + @Override + public Class getValueType() { + + return String.class; + } + + @Override + public String parse(String valueAsString, IdeContext context) { + + return valueAsString; + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/LocaleProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/LocaleProperty.java index 7bcbeac40..0db5e9750 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/LocaleProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/LocaleProperty.java @@ -53,11 +53,10 @@ public Locale parse(String valueAsString, IdeContext context) { } @Override - protected boolean completeValue(String arg, IdeContext context, Commandlet commandlet, + protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) { - int count = collector.addAllMatches(arg, getAvailableLocales(), this, commandlet); - return count > 0; + collector.addAllMatches(arg, getAvailableLocales(), this, commandlet); } private static String[] getAvailableLocales() { diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/PathProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/PathProperty.java index 96e3264d2..92c86155a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/PathProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/PathProperty.java @@ -4,6 +4,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.function.Consumer; +import java.util.stream.Stream; import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.completion.CompletionCandidateCollector; @@ -102,22 +103,20 @@ protected boolean isPathRequiredToBeFile() { } @Override - protected boolean completeValue(String arg, IdeContext context, Commandlet commandlet, + protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) { Path path = Path.of(arg); Path parent = path.getParent(); String filename = path.getFileName().toString(); if (Files.isDirectory(parent)) { - try { - Files.list(parent).filter(child -> isValidPath(path, filename)) - .forEach(child -> collector.add(child.toString(), this, commandlet)); - return true; + try (Stream children = Files.list(parent)) { + children.filter(child -> isValidPath(path, filename)) + .forEach(child -> collector.add(child.toString(), null, this, commandlet)); } catch (IOException e) { throw new IllegalStateException(e); } } - return false; } private boolean isValidPath(Path path, String filename) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/Property.java b/cli/src/main/java/com/devonfw/tools/ide/property/Property.java index 59a2bbcde..5f79211bc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/Property.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/Property.java @@ -280,7 +280,8 @@ public boolean apply(CliArguments args, IdeContext context, Commandlet commandle if (argValue == null) { argument = args.next(); if (argument.isCompletion()) { - return completeValue(argument.get(), context, commandlet, collector); + completeValue(argument.get(), context, commandlet, collector); + return true; } else { if (!argument.isEnd()) { argValue = argument.get(); @@ -326,22 +327,23 @@ protected void complete(CliArgument argument, CliArguments args, IdeContext cont String arg = argument.get(); if (this.name.isEmpty()) { - boolean match = completeValue(arg, context, commandlet, collector); - if (match) { + int count = collector.getCandidates().size(); + completeValue(arg, context, commandlet, collector); + if (collector.getCandidates().size() > count) { args.next(); } return; } if (this.name.startsWith(arg)) { - collector.add(this.name, this, commandlet); + collector.add(this.name, null, this, commandlet); } if (this.alias != null) { if (this.alias.startsWith(arg)) { - collector.add(this.alias, this, commandlet); + collector.add(this.alias, null, this, commandlet); } else if ((this.alias.length() == 2) && (this.alias.charAt(0) == '-') && argument.isShortOption()) { char opt = this.alias.charAt(1); // e.g. arg="-do" and alias="-f" -complete-> "-dof" if (arg.indexOf(opt) < 0) { - collector.add(arg + opt, this, commandlet); + collector.add(arg + opt, null, this, commandlet); } } } @@ -361,12 +363,10 @@ protected void complete(CliArgument argument, CliArguments args, IdeContext cont * @param context the {@link IdeContext}. * @param commandlet the {@link Commandlet} owning this {@link Property}. * @param collector the {@link CompletionCandidateCollector}. - * @return {@code true} if it matches, {@code false} otherwise. */ - protected boolean completeValue(String arg, IdeContext context, Commandlet commandlet, + protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) { - return true; } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/ToolProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/ToolProperty.java index a0682ca45..6b3d71e5c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/ToolProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/ToolProperty.java @@ -56,20 +56,17 @@ public ToolCommandlet parse(String valueAsString, IdeContext context) { } @Override - protected boolean completeValue(String arg, IdeContext context, Commandlet commandlet, + protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) { - boolean matches = false; for (Commandlet cmd : context.getCommandletManager().getCommandlets()) { if (cmd instanceof ToolCommandlet) { String cmdName = cmd.getName(); if (cmdName.startsWith(arg)) { - collector.add(cmdName, this, commandlet); - matches = true; + collector.add(cmdName, null, null, cmd); } } } - return matches; } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/VersionProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/VersionProperty.java index 3fa8f1fdd..5ceb204aa 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/VersionProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/VersionProperty.java @@ -1,11 +1,17 @@ package com.devonfw.tools.ide.property; +import java.util.Collections; +import java.util.List; import java.util.function.Consumer; +import java.util.stream.IntStream; import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.completion.CompletionCandidate; import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.tool.ToolCommandlet; import com.devonfw.tools.ide.version.VersionIdentifier; +import com.devonfw.tools.ide.version.VersionSegment; /** * {@link Property} for {@link VersionIdentifier} as {@link #getValueType() value type}. @@ -50,10 +56,43 @@ public VersionIdentifier parse(String valueAsString, IdeContext context) { } @Override - protected boolean completeValue(String arg, IdeContext context, Commandlet commandlet, + protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) { - return commandlet.completeVersion(VersionIdentifier.of(arg), collector); + ToolCommandlet tool = commandlet.getToolForVersionCompletion(); + if (tool != null) { + completeVersion(VersionIdentifier.of(arg), tool, context, commandlet, collector); + } } + private void completeVersion(VersionIdentifier version2complete, ToolCommandlet tool, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) { + collector.disableSorting(); + if (tool != null) { + String text; + if (version2complete == null) { + text = ""; + } else { + text = version2complete.toString(); + if (version2complete.isPattern()) { + collector.add(text, "Given version pattern.", this, commandlet); + return; + } + } + List versions = context.getUrls().getSortedVersions(tool.getName(), + tool.getEdition()); + int size = versions.size(); + String[] sorderCandidates = IntStream.rangeClosed(1, size).mapToObj(i -> versions.get(size - i).toString()) + .toArray(String[]::new); + collector.addAllMatches(text, sorderCandidates, this, commandlet); + List candidates = collector.getCandidates(); + Collections.reverse(candidates); + CompletionCandidate latest = collector.createCandidate(text + VersionSegment.PATTERN_MATCH_ANY_STABLE_VERSION, "Latest stable matching version", this, commandlet); + if (candidates.isEmpty()) { + candidates.add(latest); + } else { + candidates.add(1, latest); + } + collector.add(text + VersionSegment.PATTERN_MATCH_ANY_VERSION, "Latest matching version including unstable versions", this, commandlet); + } + } } 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 a54bd7218..2cc6b3284 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 @@ -60,14 +60,15 @@ public Path getToolBinPath() { protected boolean doInstall(boolean silent) { VersionIdentifier configuredVersion = getConfiguredVersion(); + // get installed version before installInRepo actually may install the software + VersionIdentifier installedVersion = getInstalledVersion(); VersionIdentifier selectedVersion = securityRiskInteraction(configuredVersion); setVersion(selectedVersion, silent); // install configured version of our tool in the software repository if not already installed ToolInstallation installation = installInRepo(selectedVersion); // check if we already have this version installed (linked) locally in IDE_HOME/software - VersionIdentifier installedVersion = getInstalledVersion(); VersionIdentifier resolvedVersion = installation.resolvedVersion(); - if (resolvedVersion.equals(installedVersion)) { + 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()); @@ -157,11 +158,13 @@ public ToolInstallation installInRepo(VersionIdentifier version, String edition, } catch (IOException e) { throw new IllegalStateException("Failed to write version file " + toolVersionFile, e); } - return createToolInstallation(toolPath, resolvedVersion, toolVersionFile); + // newInstallation results in above conditions to be true if isForceMode is true or if the tool version file was + // missing + return createToolInstallation(toolPath, resolvedVersion, toolVersionFile, true); } - private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier resolvedVersion, - Path toolVersionFile) { + private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier resolvedVersion, Path toolVersionFile, + boolean newInstallation) { Path linkDir = getMacOsHelper().findLinkDir(rootDir); Path binDir = linkDir; @@ -173,7 +176,13 @@ private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier assert (!linkDir.equals(rootDir)); this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE); } - return new ToolInstallation(rootDir, linkDir, binDir, resolvedVersion); + return new ToolInstallation(rootDir, linkDir, binDir, resolvedVersion, newInstallation); + } + + private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier resolvedVersion, + Path toolVersionFile) { + + return createToolInstallation(rootDir, resolvedVersion, toolVersionFile, false); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/PluginBasedCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/PluginBasedCommandlet.java index 2d92a6cec..898de06bf 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/PluginBasedCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/PluginBasedCommandlet.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -32,7 +31,7 @@ public abstract class PluginBasedCommandlet extends LocalToolCommandlet { * @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 PluginBasedCommandlet(IdeContext context, String tool, Set tags) { @@ -59,6 +58,7 @@ protected Map getPluginsMap() { } private void loadPluginsFromDirectory(Map map, Path pluginsPath) { + if (Files.isDirectory(pluginsPath)) { try (Stream childStream = Files.list(pluginsPath)) { Iterator iterator = childStream.iterator(); @@ -86,6 +86,9 @@ protected boolean isPluginUrlNeeded() { return false; } + /** + * @return the {@link Path} to the folder with the plugin configuration files inside the settings. + */ protected Path getPluginsConfigPath() { return this.context.getSettingsPath().resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS); @@ -93,10 +96,9 @@ protected Path getPluginsConfigPath() { private Path getUserHomePluginsConfigPath() { - return context.getUserHome().resolve(Paths.get(".ide", "settings", this.tool, IdeContext.FOLDER_PLUGINS)); + return this.context.getUserHome().resolve(Path.of(".ide", "settings", this.tool, IdeContext.FOLDER_PLUGINS)); } - /** * @return the immutable {@link Collection} of {@link PluginDescriptor}s configured for this IDE tool. */ 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 697bf373c..243ce86ca 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 @@ -3,7 +3,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -91,30 +90,42 @@ public final Set getTags() { @Override public void run() { - runTool(null, this.arguments.asArray()); + runTool(false, null, this.arguments.asArray()); } /** * Ensures the tool is installed and then runs this tool with the given arguments. * + * @param runInBackground {@code true}, the process of the command will be run as background process, {@code false} + * otherwise (it will be run as foreground process). * @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. - * @param args the commandline arguments to run the tool. + * @param args the command-line arguments to run the tool. */ - public void runTool(VersionIdentifier toolVersion, String... args) { + public void runTool(boolean runInBackground, VersionIdentifier toolVersion, String... args) { Path binaryPath; - Path toolPath = Paths.get(getBinaryName()); + Path toolPath = Path.of(getBinaryName()); if (toolVersion == null) { install(true); binaryPath = toolPath; } else { throw new UnsupportedOperationException("Not yet implemented!"); } - ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(binaryPath) - .addArgs(args); - pc.run(); + ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(binaryPath).addArgs(args); + + pc.run(false, runInBackground); + } + + /** + * @param toolVersion the explicit {@link VersionIdentifier} of the tool to run. + * @param args the command-line arguments to run the tool. + * @see ToolCommandlet#runTool(boolean, VersionIdentifier, String...) + */ + public void runTool(VersionIdentifier toolVersion, String... args) { + + runTool(false, toolVersion, args); } /** @@ -344,12 +355,11 @@ private Path getProperInstallationSubDirOf(Path path) { try (Stream stream = Files.list(path)) { Path[] subFiles = stream.toArray(Path[]::new); if (subFiles.length == 0) { - throw new CliException("The downloaded package for the tool " + this.tool - + " seems to be empty as you can check in the extracted folder " + path); + throw new CliException("The downloaded package for the tool " + this.tool + " seems to be empty as you can check in the extracted folder " + path); } else if (subFiles.length == 1) { String filename = subFiles[0].getFileName().toString(); - if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS) - && !filename.endsWith(".app") && Files.isDirectory(subFiles[0])) { + if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS) && !filename.endsWith(".app") + && Files.isDirectory(subFiles[0])) { return getProperInstallationSubDirOf(subFiles[0]); } } @@ -391,10 +401,6 @@ protected void extract(Path file, Path targetDir) { fileAccess.copy(appPath, tmpDir); pc.addArgs("detach", "-force", mountPath); pc.run(); - // if [ -e "${target_dir}/Applications" ] - // then - // rm "${target_dir}/Applications" - // fi } else if ("msi".equals(extension)) { this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + tmpDir).run(); // msiexec also creates a copy of the MSI @@ -412,14 +418,36 @@ protected void extract(Path file, Path targetDir) { } else { throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + file); } - fileAccess.move(getProperInstallationSubDirOf(tmpDir), targetDir); + moveAndProcessExtraction(getProperInstallationSubDirOf(tmpDir), targetDir); fileAccess.delete(tmpDir); } else { this.context.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.", getName(), file); + + if (Files.isDirectory(file)) { fileAccess.move(file, targetDir); + } else { + try { + Files.createDirectories(targetDir); + } catch (IOException e) { + throw new IllegalStateException("Failed to create folder " + targetDir); + } + fileAccess.move(file, targetDir.resolve(file.getFileName())); + } } } + /** + * Moves the extracted content to the final destination {@link Path}. May be overridden to customize the extraction + * process. + * + * @param from the source {@link Path} to move. + * @param to the target {@link Path} to move to. + */ + protected void moveAndProcessExtraction(Path from, Path to) { + + this.context.getFileAccess().move(from, to); + } + /** * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise. */ @@ -475,6 +503,49 @@ protected VersionIdentifier getInstalledVersion(Path toolPath) { } } + /** + * @return the installed edition of this tool or {@code null} if not installed. + */ + public String getInstalledEdition() { + + return getInstalledEdition(this.context.getSoftwarePath().resolve(getName())); + } + + /** + * @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. + * @return the installed edition of this tool or {@code null} if not installed. + */ + public String getInstalledEdition(Path toolPath) { + + if (!Files.isDirectory(toolPath)) { + this.context.debug("Tool {} not installed in {}", getName(), toolPath); + return null; + } + try { + String edition = toolPath.toRealPath().getParent().getFileName().toString(); + if (!this.context.getUrls().getSortedEditions(getName()).contains(edition)) { + edition = getEdition(); + } + 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); + } + + } + + /** + * List the available editions of this tool. + */ + public void listEditions() { + + List editions = this.context.getUrls().getSortedEditions(getName()); + for (String edition : editions) { + this.context.info(edition); + } + } + /** * List the available versions of this tool. */ @@ -524,9 +595,8 @@ public void setVersion(VersionIdentifier version, boolean hint) { this.context.info("{}={} has been set in {}", name, version, settingsVariables.getSource()); EnvironmentVariables declaringVariables = variables.findVariable(name); if ((declaringVariables != null) && (declaringVariables != settingsVariables)) { - this.context.warning( - "The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", - name, declaringVariables.getSource()); + this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name, + declaringVariables.getSource()); } if (hint) { this.context.info("To install that version call the following command:"); @@ -534,4 +604,48 @@ public void setVersion(VersionIdentifier version, boolean hint) { } } + /** + * Sets the tool edition in the environment variable configuration file. + * + * @param edition the edition to set. + */ + public void setEdition(String edition) { + + setEdition(edition, true); + } + + /** + * Sets the tool edition in the environment variable configuration file. + * + * @param edition the edition to set + * @param hint - {@code true} to print the installation hint, {@code false} otherwise. + */ + public void setEdition(String edition, boolean hint) { + + if ((edition == null) || edition.isBlank()) { + throw new IllegalStateException("Edition has to be specified!"); + } + + if (!Files.exists(this.context.getUrls().getEdition(getName(), edition).getPath())) { + this.context.warning("Edition {} seems to be invalid", edition); + + } + EnvironmentVariables variables = this.context.getVariables(); + EnvironmentVariables settingsVariables = variables.getByType(EnvironmentVariablesType.SETTINGS); + String name = EnvironmentVariables.getToolEditionVariable(this.tool); + settingsVariables.set(name, edition, false); + settingsVariables.save(); + + this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource()); + EnvironmentVariables declaringVariables = variables.findVariable(name); + if ((declaringVariables != null) && (declaringVariables != settingsVariables)) { + this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name, + declaringVariables.getSource()); + } + if (hint) { + this.context.info("To install that edition call the following command:"); + this.context.info("ide install {}", this.tool); + } + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java index 6522e92ff..4276b962c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java @@ -13,7 +13,9 @@ * @param binDir the {@link Path} relative to {@code linkDir} pointing to the directory containing the binaries that * should be put on the path (typically "bin"). * @param resolvedVersion the {@link VersionIdentifier} of the resolved tool version installed in {@code rootDir}. + * @param newInstallation {@code true} - if the tool should be installed newly, {@code true} - else */ -public record ToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier resolvedVersion) { +public record ToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier resolvedVersion, + boolean newInstallation) { } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidJsonDownload.java b/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidJsonDownload.java index 314cf06eb..950446bd7 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidJsonDownload.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidJsonDownload.java @@ -20,7 +20,7 @@ public class AndroidJsonDownload implements JsonObject { */ public String getLink() { - return link; + return this.link; } /** @@ -28,7 +28,7 @@ public String getLink() { */ public String getChecksum() { - return checksum; + return this.checksum; } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidJsonItem.java b/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidJsonItem.java index a343825c8..0ae04ae69 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidJsonItem.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidJsonItem.java @@ -29,7 +29,7 @@ public String getVersion() { */ public List getDownload() { - return download; + return this.download; } } \ No newline at end of file diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/aws/Aws.java b/cli/src/main/java/com/devonfw/tools/ide/tool/aws/Aws.java new file mode 100644 index 000000000..d2257228f --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/aws/Aws.java @@ -0,0 +1,100 @@ +package com.devonfw.tools.ide.tool.aws; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; + +import com.devonfw.tools.ide.common.Tag; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.environment.EnvironmentVariablesType; +import com.devonfw.tools.ide.process.ProcessContext; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; + +/** + * {@link LocalToolCommandlet} for AWS CLI (aws). + * + * @see AWS CLI homepage + */ + +public class Aws extends LocalToolCommandlet { + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public Aws(IdeContext context) { + + super(context, "aws", Set.of(Tag.CLOUD)); + } + + private void makeExecutable(Path file) { + + // TODO this can be removed if issue #132 is fixed. See https://github.com/devonfw/IDEasy/issues/132 + Set permissions = null; + try { + permissions = Files.getPosixFilePermissions(file); + permissions.add(PosixFilePermission.GROUP_EXECUTE); + permissions.add(PosixFilePermission.OWNER_EXECUTE); + permissions.add(PosixFilePermission.OTHERS_EXECUTE); + Files.setPosixFilePermissions(file, permissions); + } catch (IOException e) { + throw new RuntimeException("Adding execution permission for Group, Owner and Others did not work for " + file, e); + } + } + + @Override + protected void moveAndProcessExtraction(Path from, Path to) { + + if (this.context.getSystemInfo().isLinux()) { + // make binary executable using java nio because unpacking didn't preserve the file permissions + // TODO this can be removed if issue #132 is fixed + Path awsInDistPath = from.resolve("dist").resolve("aws"); + Path awsCompleterInDistPath = from.resolve("dist").resolve("aws_completer"); + makeExecutable(awsInDistPath); + makeExecutable(awsCompleterInDistPath); + + // running the install-script that aws shipped + ProcessContext pc = this.context.newProcess(); + Path linuxInstallScript = from.resolve("install"); + pc.executable(linuxInstallScript); + pc.addArgs("-i", from.toString(), "-b", from.toString()); + pc.run(); + + // The install-script that aws ships creates symbolic links to binaries but using absolute paths. + // Since the current process happens in a temporary dir, these links wouldn't be valid after moving the + // installation files to the target dir. So the absolute paths are replaced by relative ones. + for (String file : new String[] { "aws", "aws_completer", Path.of("v2").resolve("current").toString() }) { + Path link = from.resolve(file); + try { + this.context.getFileAccess().symlink(link.toRealPath(), link, true); + } catch (IOException e) { + throw new RuntimeException( + "Failed to replace absolute link (" + link + ") provided by AWS install script with relative link.", e); + } + } + this.context.getFileAccess().delete(linuxInstallScript); + this.context.getFileAccess().delete(from.resolve("dist")); + } + super.moveAndProcessExtraction(from, to); + } + + @Override + public void postInstall() { + + super.postInstall(); + + EnvironmentVariables variables = this.context.getVariables(); + EnvironmentVariables typeVariables = variables.getByType(EnvironmentVariablesType.CONF); + Path awsConfigDir = this.context.getConfPath().resolve("aws"); + this.context.getFileAccess().mkdirs(awsConfigDir); + Path awsConfigFile = awsConfigDir.resolve("config"); + Path awsCredentialsFile = awsConfigDir.resolve("credentials"); + typeVariables.set("AWS_CONFIG_FILE", awsConfigFile.toString(), true); + typeVariables.set("AWS_SHARED_CREDENTIALS_FILE", awsCredentialsFile.toString(), true); + typeVariables.save(); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/az/Azure.java b/cli/src/main/java/com/devonfw/tools/ide/tool/az/Azure.java index 195ccf922..efe9681a9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/az/Azure.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/az/Azure.java @@ -1,6 +1,6 @@ package com.devonfw.tools.ide.tool.az; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.Set; import com.devonfw.tools.ide.common.Tag; @@ -35,6 +35,6 @@ public void postInstall() { EnvironmentVariables typeVariables = variables.getByType(EnvironmentVariablesType.CONF); typeVariables.set("AZURE_CONFIG_DIR", this.context.getConfPath().resolve(".azure").toString(), true); typeVariables.save(); - this.context.getFileAccess().symlink(Paths.get("wbin"), getToolPath().resolve("bin")); + this.context.getFileAccess().symlink(Path.of("wbin"), getToolPath().resolve("bin")); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/cobigen/Cobigen.java b/cli/src/main/java/com/devonfw/tools/ide/tool/cobigen/Cobigen.java new file mode 100644 index 000000000..73f338aa7 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/cobigen/Cobigen.java @@ -0,0 +1,33 @@ +package com.devonfw.tools.ide.tool.cobigen; + +import java.util.Set; + +import com.devonfw.tools.ide.common.Tag; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; +import com.devonfw.tools.ide.tool.ToolCommandlet; +import com.devonfw.tools.ide.tool.mvn.Mvn; + +/** + * {@link ToolCommandlet} for cobigen CLI (cobigen). + */ +public class Cobigen extends LocalToolCommandlet { + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public Cobigen(IdeContext context) { + + super(context, "cobigen", Set.of(Tag.GENERATOR)); + } + + @Override + public boolean install(boolean silent) { + + getCommandlet(Mvn.class).install(); + return super.install(silent); + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/eclipse/Eclipse.java b/cli/src/main/java/com/devonfw/tools/ide/tool/eclipse/Eclipse.java index 5d91ad304..133c65d31 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/eclipse/Eclipse.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/eclipse/Eclipse.java @@ -5,7 +5,6 @@ import java.nio.channels.FileLock; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.Set; @@ -61,7 +60,7 @@ public boolean install(boolean silent) { */ protected ProcessResult runEclipse(boolean log, String... args) { - Path toolPath = Paths.get(getBinaryName()); + Path toolPath = Path.of(getBinaryName()); ProcessContext pc = this.context.newProcess(); if (log) { pc.errorHandling(ProcessErrorHandling.ERROR); @@ -81,7 +80,7 @@ protected ProcessResult runEclipse(boolean log, String... args) { Path javaPath = getCommandlet(Java.class).getToolBinPath(); pc.addArg("-vm").addArg(javaPath); pc.addArgs(args); - return pc.run(log); + return pc.run(log, false); } @Override diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/gcviewer/GcViewer.java b/cli/src/main/java/com/devonfw/tools/ide/tool/gcviewer/GcViewer.java new file mode 100644 index 000000000..300a6f909 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/gcviewer/GcViewer.java @@ -0,0 +1,43 @@ +package com.devonfw.tools.ide.tool.gcviewer; + +import java.util.Set; + +import com.devonfw.tools.ide.common.Tag; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.process.ProcessContext; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; +import com.devonfw.tools.ide.tool.ToolCommandlet; + +/** + * {@link ToolCommandlet} for GcViewer. + */ +public class GcViewer extends LocalToolCommandlet { + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public GcViewer(IdeContext context) { + + super(context, "gcviewer", Set.of(Tag.JAVA)); + } + + @Override + protected boolean isExtract() { + + return false; + } + + @Override + public void run() { + + install(true); + ProcessContext pc = this.context.newProcess(); + pc.directory(getToolPath()); + pc.executable("java"); + pc.addArg("-jar"); + pc.addArg("gcviewer-" + getInstalledVersion() + ".jar"); + pc.run(); + } +} \ No newline at end of file diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/gradle/GradleUrlUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/tool/gradle/GradleUrlUpdater.java index aa0f99ece..ad6338483 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/gradle/GradleUrlUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/gradle/GradleUrlUpdater.java @@ -53,13 +53,13 @@ protected String getEdition() { @Override protected void addVersion(UrlVersion urlVersion) { - if (responseBody == null) { - responseBody = doGetResponseBodyAsString(HASH_VERSION_URL); + if (this.responseBody == null) { + this.responseBody = doGetResponseBodyAsString(HASH_VERSION_URL); } String hashSum = ""; - if (responseBody != null && !responseBody.isEmpty()) { - hashSum = doGetHashSumForVersion(responseBody, urlVersion.getName()); + if (this.responseBody != null && !this.responseBody.isEmpty()) { + hashSum = doGetHashSumForVersion(this.responseBody, urlVersion.getName()); } if (hashSum.isEmpty()) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ide/IdeToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ide/IdeToolCommandlet.java index 2cd8ec18b..4bb4800c8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/ide/IdeToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ide/IdeToolCommandlet.java @@ -207,7 +207,7 @@ public void run() { */ protected void runIde(String... args) { - runTool(null, args); + runTool(false,null, args); } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/jmc/Jmc.java b/cli/src/main/java/com/devonfw/tools/ide/tool/jmc/Jmc.java new file mode 100644 index 000000000..0c316cfbf --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/jmc/Jmc.java @@ -0,0 +1,80 @@ +package com.devonfw.tools.ide.tool.jmc; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Stream; + +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.tool.LocalToolCommandlet; +import com.devonfw.tools.ide.tool.ToolCommandlet; +import com.devonfw.tools.ide.tool.java.Java; + +/** + * {@link ToolCommandlet} for JDK Mission + * Control, An advanced set of tools for managing, monitoring, profiling, and troubleshooting Java applications. + */ +public class Jmc extends LocalToolCommandlet { + + /** + * The constructor. + * + * @param context the {@link IdeContext}. method. + */ + public Jmc(IdeContext context) { + + super(context, "jmc", Set.of(Tag.JAVA, Tag.QA, Tag.ANALYSE, Tag.JVM)); + } + + @Override + public boolean doInstall(boolean silent) { + + // TODO https://github.com/devonfw/IDEasy/issues/209 currently outcommented as this breaks the tests, real fix needed asap + // getCommandlet(Java.class).install(); + return super.doInstall(silent); + } + + @Override + public void run() { + + runTool(true, null, this.arguments.asArray()); + } + + @Override + public void postInstall() { + + super.postInstall(); + + if (this.context.getSystemInfo().isWindows() || this.context.getSystemInfo().isLinux()) { + Path toolPath = getToolPath(); + Path oldBinaryPath = toolPath.resolve("JDK Mission Control"); + if (Files.isDirectory(oldBinaryPath)) { + FileAccess fileAccess = this.context.getFileAccess(); + moveFilesAndDirs(oldBinaryPath, toolPath); + fileAccess.delete(oldBinaryPath); + } else { + this.context.info("JMC binary folder not found at {} - ignoring as this legacy problem may be resolved in newer versions.", oldBinaryPath); + } + } + + } + + private void moveFilesAndDirs(Path sourceFolder, Path targetFolder) { + + FileAccess fileAccess = this.context.getFileAccess(); + try (Stream childStream = Files.list(sourceFolder)) { + Iterator iterator = childStream.iterator(); + while (iterator.hasNext()) { + Path child = iterator.next(); + fileAccess.move(child, targetFolder.resolve(child.getFileName())); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to list files to move in " + sourceFolder, e); + } + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/terraform/Terraform.java b/cli/src/main/java/com/devonfw/tools/ide/tool/terraform/Terraform.java index 745ececde..e99d1264b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/terraform/Terraform.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/terraform/Terraform.java @@ -26,6 +26,6 @@ public Terraform(IdeContext context) { protected void postInstall() { super.postInstall(); - runTool(null, "-install-autocomplete"); + runTool(false, null, "-install-autocomplete"); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java b/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java index 1597c7ec2..ab2f90369 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java +++ b/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java @@ -6,7 +6,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.context.IdeContext; @@ -52,6 +51,25 @@ public UrlEdition getEdition(String tool, String edition) { return urlEdition; } + /** + * @param tool the name of the {@link UrlTool}. + * @return the sorted {@link List} of {@link String editions} . + */ + public List getSortedEditions(String tool) { + + List list = new ArrayList<>(); + UrlTool urlTool = this.repository.getChild(tool); + if (urlTool == null) { + this.context.warning("Can't get sorted editions for tool {} because it does not exist in {}.", tool, this.repository.getPath()); + } else { + for (UrlEdition urlEdition : urlTool.getChildren()) { + list.add(urlEdition.getName()); + } + } + Collections.sort(list); + return Collections.unmodifiableList(list); + } + /** * @param tool the name of the {@link UrlTool}. * @param edition the name of the {@link UrlEdition}. @@ -99,9 +117,8 @@ public VersionIdentifier getVersion(String tool, String edition, VersionIdentifi return vi; } } - throw new CliException("Could not find any version matching '" + version + "' for tool '" + tool - + "' - potentially there are " + versions.size() + " version(s) available in " - + getEdition(tool, edition).getPath() + " but none matched!"); + throw new CliException("Could not find any version matching '" + version + "' for tool '" + tool + "' - potentially there are " + versions.size() + + " version(s) available in " + getEdition(tool, edition).getPath() + " but none matched!"); } /** @@ -116,8 +133,7 @@ public UrlVersion getVersionFolder(String tool, String edition, VersionIdentifie VersionIdentifier resolvedVersion = getVersion(tool, edition, version); UrlVersion urlVersion = getEdition(tool, edition).getChild(resolvedVersion.toString()); if (urlVersion == null) { - throw new IllegalArgumentException( - "Version " + version + " for tool " + tool + " does not exist in edition " + edition + "."); + throw new IllegalArgumentException("Version " + version + " for tool " + tool + " does not exist in edition " + edition + "."); } return urlVersion; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/url/updater/AbstractUrlUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/url/updater/AbstractUrlUpdater.java index 21abd9428..c8e797910 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/url/updater/AbstractUrlUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/url/updater/AbstractUrlUpdater.java @@ -86,6 +86,7 @@ public abstract class AbstractUrlUpdater extends AbstractProcessorWithTimeout im protected final String getToolWithEdition() { String tool = getTool(); + String edition = getEdition(); if (tool.equals(edition)) { return tool; diff --git a/cli/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java b/cli/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java index a97ecf68b..ae3b42c13 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java +++ b/cli/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java @@ -18,8 +18,8 @@ import com.devonfw.tools.ide.tool.docker.DockerRancherDesktopUrlUpdater; import com.devonfw.tools.ide.tool.dotnet.DotNetUrlUpdater; import com.devonfw.tools.ide.tool.eclipse.EclipseCppUrlUpdater; -import com.devonfw.tools.ide.tool.eclipse.EclipseJeeUrlUpdater; import com.devonfw.tools.ide.tool.eclipse.EclipseJavaUrlUpdater; +import com.devonfw.tools.ide.tool.eclipse.EclipseJeeUrlUpdater; import com.devonfw.tools.ide.tool.gcloud.GCloudUrlUpdater; import com.devonfw.tools.ide.tool.gcviewer.GcViewerUrlUpdater; import com.devonfw.tools.ide.tool.gh.GhUrlUpdater; @@ -46,6 +46,7 @@ import com.devonfw.tools.ide.tool.tomcat.TomcatUrlUpdater; import com.devonfw.tools.ide.tool.vscode.VsCodeUrlUpdater; import com.devonfw.tools.ide.url.model.folder.UrlRepository; +import com.devonfw.tools.ide.tool.jasypt.JasyptUrlUpdater; /** * The {@code UpdateManager} class manages the update process for various tools by using a list of @@ -63,12 +64,12 @@ public class UpdateManager extends AbstractProcessorWithTimeout { new AzureUrlUpdater(), new CobigenUrlUpdater(), new DockerDesktopUrlUpdater(), new DotNetUrlUpdater(), new EclipseCppUrlUpdater(), new EclipseJeeUrlUpdater(), new EclipseJavaUrlUpdater(), new GCloudUrlUpdater(), new GcViewerUrlUpdater(), new GhUrlUpdater(), new GraalVmCommunityUpdater(), new GraalVmOracleUrlUpdater(), - new GradleUrlUpdater(), new HelmUrlUpdater(), new IntellijCommunityUrlUpdater(), new IntellijUltimateUrlUpdater(), - new JavaUrlUpdater(), new JenkinsUrlUpdater(), new JmcUrlUpdater(), new KotlincUrlUpdater(), - new KotlincNativeUrlUpdater(), new LazyDockerUrlUpdater(), new MvnUrlUpdater(), new NodeUrlUpdater(), - new NpmUrlUpdater(), new OcUrlUpdater(), new PipUrlUpdater(), new PythonUrlUpdater(), new QuarkusUrlUpdater(), - new DockerRancherDesktopUrlUpdater(), new SonarUrlUpdater(), new TerraformUrlUpdater(), new TomcatUrlUpdater(), - new VsCodeUrlUpdater()); + new GradleUrlUpdater(), new HelmUrlUpdater(), new IntellijUrlUpdater(), new JavaUrlUpdater(), + new JenkinsUrlUpdater(), new JmcUrlUpdater(), new KotlincUrlUpdater(), new KotlincNativeUrlUpdater(), + new LazyDockerUrlUpdater(), new MvnUrlUpdater(), new NodeUrlUpdater(), new NpmUrlUpdater(), new OcUrlUpdater(), + new PipUrlUpdater(), new PythonUrlUpdater(), new QuarkusUrlUpdater(), new DockerRancherDesktopUrlUpdater(), + new SonarUrlUpdater(), new TerraformUrlUpdater(), new TomcatUrlUpdater(), new VsCodeUrlUpdater(), + new JasyptUrlUpdater()); /** * The constructor. diff --git a/cli/src/main/java/com/devonfw/tools/ide/util/FilenameUtil.java b/cli/src/main/java/com/devonfw/tools/ide/util/FilenameUtil.java index e0a2d2add..283e35deb 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/util/FilenameUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/util/FilenameUtil.java @@ -28,6 +28,13 @@ public static String getExtension(String path) { lastSlash = 0; } int lastDot = path.lastIndexOf('.'); + + // workaround for sourceforge urls ending with /download like + // https://sourceforge.net/projects/gcviewer/files/gcviewer-1.36.jar/download + if (path.startsWith("https://") && path.contains("sourceforge") && path.endsWith("download")) { + return path.substring(lastDot + 1, lastSlash); + } + if (lastDot < lastSlash) { return null; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinitionPath.java b/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinitionPath.java index c60329fc5..0840d13e9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinitionPath.java +++ b/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinitionPath.java @@ -1,7 +1,6 @@ package com.devonfw.tools.ide.variable; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.function.Function; import com.devonfw.tools.ide.context.IdeContext; @@ -67,6 +66,6 @@ public Class getValueType() { @Override public Path fromString(String value, IdeContext context) { - return Paths.get(value); + return Path.of(value); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/VersionComparisonResult.java b/cli/src/main/java/com/devonfw/tools/ide/version/VersionComparisonResult.java index ec69bcdbc..2452a5cb9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/version/VersionComparisonResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/version/VersionComparisonResult.java @@ -65,19 +65,12 @@ public boolean isGreater() { */ public int asValue() { - switch (this) { - case LESS: - case LESS_UNSAFE: - return -1; - case EQUAL: - case EQUAL_UNSAFE: - return 0; - case GREATER: - case GREATER_UNSAFE: - return 1; - default: - throw new IllegalStateException(toString()); - } + return switch (this) { + case LESS, LESS_UNSAFE -> -1; + case EQUAL, EQUAL_UNSAFE -> 0; + case GREATER, GREATER_UNSAFE -> 1; + default -> throw new IllegalStateException(toString()); + }; } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/VersionLetters.java b/cli/src/main/java/com/devonfw/tools/ide/version/VersionLetters.java index d6537b611..80b6a3966 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/version/VersionLetters.java +++ b/cli/src/main/java/com/devonfw/tools/ide/version/VersionLetters.java @@ -171,10 +171,7 @@ public VersionMatchResult matches(VersionLetters other, boolean pattern) { } return VersionMatchResult.MATCH; } else { - if (this.phase != other.phase) { - return VersionMatchResult.MISMATCH; - } - if (!this.lettersLowerCase.equals(other.lettersLowerCase)) { + if ((this.phase != other.phase) || !this.lettersLowerCase.equals(other.lettersLowerCase)) { return VersionMatchResult.MISMATCH; } return VersionMatchResult.EQUAL; diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java b/cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java index 0c84b68e2..fb47384f5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java +++ b/cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java @@ -261,10 +261,7 @@ public VersionMatchResult matches(VersionSegment other) { } } } else { - if (this.number != other.number) { - return VersionMatchResult.MISMATCH; - } - if (!this.separator.equals(other.separator)) { + if ((this.number != other.number) || !this.separator.equals(other.separator)) { return VersionMatchResult.MISMATCH; } } diff --git a/cli/src/main/resources/nls/Ide.properties b/cli/src/main/resources/nls/Ide.properties index cbbc9a9b0..ef6b3eda7 100644 --- a/cli/src/main/resources/nls/Ide.properties +++ b/cli/src/main/resources/nls/Ide.properties @@ -2,31 +2,40 @@ usage=Usage: values=Values: commandlets=Available commandlets: options=Options: +cmd-aws=Tool commandlet for AWS CLI. +cmd-az=Tool commandlet for Azure CLI. cmd---version=Print the version of IDEasy. -cmd-az=Tool commandlet for Azure CLI cmd-complete=Internal commandlet for bash auto-completion cmd-eclipse=Tool commandlet for Eclipse (IDE) cmd-env=Print the environment variables to set and export. +cmd-get-edition=Get the edition of the selected tool. cmd-get-version=Get the version of the selected tool. -cmd-gh=Tool commandlet for Github CLI +cmd-gh=Tool commandlet for Github CLI. cmd-gradle=Tool commandlet for Gradle (Build-Tool) cmd-helm=Tool commandlet for Helm (Kubernetes Package Manager) cmd-help=Prints this help. cmd-install=Install the selected tool. cmd-java=Tool commandlet for Java (OpenJDK) -cmd-kotlinc=Tool commandlet for Kotlin -cmd-kotlincnative=Tool commandlet for Kotlin-Native +cmd-jmc=Tool commandlet for JDK Mission Control +cmd-kotlinc=Tool commandlet for Kotlin. +cmd-kotlincnative=Tool commandlet for Kotlin-Native. cmd-list-version=List the available versions of the selected tool. +cmd-list-editions=List the available editions of the selected tool. +cmd-list-versions=List the available versions of the selected tool. cmd-mvn=Tool commandlet for Maven (Build-Tool) cmd-node=Tool commandlet for Node.js (JavaScript runtime) cmd-oc=Tool commandlet for Openshift CLI (Kubernetes Management Tool) cmd-quarkus=Tool commandlet for Quarkus (Framework for cloud-native apps) +cmd-set-edition=Set the edition of the selected tool. cmd-set-version=Set the version of the selected tool. -cmd-terraform=Tool commandlet for Terraform. +cmd-shell=Commandlet to start built-in shell with advanced auto-completion. +cmd-terraform=Tool commandlet for Terraform cmd-vscode=Tool commandlet for Visual Studio Code (IDE) +cmd-cobigen=Tool commandlet for Cobigen val-args=The commandline arguments to pass to the tool. +val-edition=The tool edition. val-tool=The tool commandlet to select. -val-version=The tool version. +val-version=The tool version val-set-version-version=The tool version to set. version-banner=Current version of IDE is {} opt--batch=enable batch mode (non-interactive) diff --git a/cli/src/main/resources/nls/Ide_de.properties b/cli/src/main/resources/nls/Ide_de.properties index cb895c7e6..688bd923a 100644 --- a/cli/src/main/resources/nls/Ide_de.properties +++ b/cli/src/main/resources/nls/Ide_de.properties @@ -2,27 +2,35 @@ usage=Verwendung: values=Werte: commandlets=Verfügbare Kommandos: options=Optionen: +cmd-aws=Werkzeug Kommando fuer AWS Kommandoschnittstelle. +cmd-az=Werkzeug Kommando fuer Azure Kommandoschnittstelle. cmd---version=Gibt die Version von IDEasy aus. -cmd-az=Werkzeug Kommando für die Azure Kommandoschnittstelle. cmd-eclipse=Werkzeug Kommando für Eclipse (IDE) cmd-env=Gibt die zu setztenden und exportierenden Umgebungsvariablen aus. +cmd-get-edition=Zeigt die Edition des selektierten Werkzeugs an. cmd-get-version=Zeigt die Version des selektierten Werkzeugs an. -cmd-gh=Werkzeug Kommando für die Github Kommandoschnittstelle +cmd-gh=Werkzeug Kommando für die Github Kommandoschnittstelle. cmd-helm=Werkzeug Kommando für Helm (Kubernetes Package Manager) cmd-help=Zeigt diese Hilfe an. cmd-install=Installiert das selektierte Werkzeug. cmd-java=Werkzeug Kommando für Java (OpenJDK) -cmd-kotlinc=Werkzeug Kommando für Kotlin -cmd-kotlincnative=Werkzeug Kommando für Kotlin-Native +cmd-jmc=Werkzeug Kommando für JDK Mission Control +cmd-kotlinc=Werkzeug Kommando für Kotlin. +cmd-kotlincnative=Werkzeug Kommando für Kotlin-Native. cmd-list-version=Listet die verfügbaren Versionen des selektierten Werkzeugs auf. +cmd-list-editions=Listet die verfügbaren Editionen des selektierten Werkzeugs auf. +cmd-list-versions=Listet die verfügbaren Versionen des selektierten Werkzeugs auf. cmd-mvn=Werkzeug Kommando für Maven (Build-Werkzeug) cmd-node=Werkzeug Kommando für Node.js (JavaScript Laufzeitumgebung) cmd-oc=Werkzeug Kommando für Openshift CLI (Kubernetes Management Tool) cmd-quarkus=Werkzeug Kommando für Quarkus (Framework für Cloud-native Anwendungen) +cmd-set-edition=Setzt die Edition des selektierten Werkzeugs. cmd-set-version=Setzt die Version des selektierten Werkzeugs. cmd-terraform=Werkzeug Kommando für Terraform. cmd-vscode=Werkzeug Kommando für Visual Studio Code (IDE) +cmd-cobigen=Werkzeug Kommando für Cobigen. val-args=Die Kommandozeilen-Argumente zur Übergabe an das Werkzeug. +val-edition=Die Werkzeug Edition. val-tool=Das zu selektierende Werkzeug Kommando. val-version=Die Werkzeug Version. val-set-version-version=Die zu setztende Werkzeug Version. diff --git a/cli/src/test/java/com/devonfw/tools/ide/cli/AutocompletionReaderTestSupport.java b/cli/src/test/java/com/devonfw/tools/ide/cli/AutocompletionReaderTestSupport.java new file mode 100644 index 000000000..043e04790 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/cli/AutocompletionReaderTestSupport.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2002-2017, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package com.devonfw.tools.ide.cli; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jline.reader.Candidate; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.impl.LineReaderImpl; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.impl.DumbTerminal; +import org.junit.jupiter.api.BeforeEach; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; + +/** + * Provides support for reader and completion tests. Inspired by jline3 + */ +public abstract class AutocompletionReaderTestSupport extends AbstractIdeContextTest { + + private static final String TAB = "\011"; + + protected Terminal terminal; + + protected TestLineReader reader; + + protected EofPipedInputStream in; + + protected ByteArrayOutputStream out; + + protected Character mask; + + @BeforeEach + public void setUp() throws Exception { + + Handler ch = new ConsoleHandler(); + ch.setLevel(Level.FINEST); + Logger logger = Logger.getLogger("org.jline"); + logger.addHandler(ch); + // Set the handler log level + logger.setLevel(Level.INFO); + + this.in = new EofPipedInputStream(); + this.out = new ByteArrayOutputStream(); + this.terminal = new DumbTerminal("terminal", "ansi", this.in, this.out, StandardCharsets.UTF_8); + this.terminal.setSize(new Size(160, 80)); + this.reader = new TestLineReader(this.terminal, "JLine", null); + this.reader.setKeyMap(LineReader.EMACS); + this.mask = null; + } + + protected void assertBuffer(final String expected, final TestBuffer buffer) { + + assertBuffer(expected, buffer, true); + } + + protected void assertBuffer(final String expected, final TestBuffer buffer, final boolean clear) { + + // clear current buffer, if any + if (clear) { + try { + this.reader.getHistory().purge(); + } catch (IOException e) { + throw new IllegalStateException("Failed to purge history.", e); + } + } + this.reader.list = false; + this.reader.menu = false; + + this.in.setIn(new ByteArrayInputStream(buffer.getBytes())); + + // run it through the reader + try { + while (true) { + this.reader.readLine(null, null, this.mask, null); + } + } catch (EndOfFileException e) { + // noop + } + + assertThat(this.reader.getBuffer().toString()).isEqualTo(expected); + } + + protected class TestBuffer { + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + public TestBuffer(String str) { + + append(str); + } + + @Override + public String toString() { + + return this.out.toString(StandardCharsets.UTF_8); + } + + public byte[] getBytes() { + + return this.out.toByteArray(); + } + + public TestBuffer tab() { + + return append(TAB); + } + + public TestBuffer append(final String str) { + + for (byte b : str.getBytes(StandardCharsets.UTF_8)) { + append(b); + } + return this; + } + + public TestBuffer append(final int i) { + + this.out.write((byte) i); + return this; + } + } + + public static class EofPipedInputStream extends InputStream { + + private InputStream in; + + public void setIn(InputStream in) { + + this.in = in; + } + + @Override + public int read() throws IOException { + + return this.in != null ? this.in.read() : -1; + } + + @Override + public int available() throws IOException { + + return this.in != null ? this.in.available() : 0; + } + } + + public static class TestLineReader extends LineReaderImpl { + boolean list = false; + + boolean menu = false; + + public TestLineReader(Terminal terminal, String appName, Map variables) { + + super(terminal, appName, variables); + } + + @Override + protected boolean doList(List possible, String completed, boolean runLoop, + BiFunction escaper) { + + this.list = true; + return super.doList(possible, completed, runLoop, escaper); + } + + @Override + protected boolean doMenu(List possible, String completed, + BiFunction escaper) { + + this.menu = true; + return super.doMenu(possible, completed, escaper); + } + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ContextCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ContextCommandletTest.java index f1f02c21e..9a518ae60 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ContextCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ContextCommandletTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; -import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeContextConsole; diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionGetCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionGetCommandletTest.java new file mode 100644 index 000000000..b4e239b3c --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionGetCommandletTest.java @@ -0,0 +1,67 @@ +package com.devonfw.tools.ide.commandlet; + +import java.nio.file.Path; +import java.util.List; + +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; + +/** Integration test of {@link EditionGetCommandlet}. */ + +public class EditionGetCommandletTest extends AbstractIdeContextTest { + + /** Test of {@link VersionGetCommandlet} run. */ + @Test + public void testEditionGetCommandletRun() { + + // arrange + String path = "workspaces/foo-test/my-git-repo"; + String tool = "az"; + IdeTestContext context = newContext("basic", path, true); + mockInstallTool(context, tool); + EditionGetCommandlet editionGet = context.getCommandletManager().getCommandlet(EditionGetCommandlet.class); + + // act + editionGet.tool.setValueAsString(tool, context); + editionGet.run(); + + // assert + List logs = context.level(IdeLogLevel.INFO).getMessages(); + assertThat(logs).contains("az"); + } + + /** + * Mocks the installation of a tool, since getEdition depends on symlinks which are not distributed with git + * + * @param context the {@link IdeContext} to use. + * @param tool the tool to mock install. + */ + private static void mockInstallTool(IdeTestContext context, String tool) { + + Path pathToInstallationOfDummyTool = context.getSoftwareRepositoryPath() + .resolve(context.getDefaultToolRepository().getId()).resolve(tool).resolve("az/testVersion"); + Path pathToLinkedSoftware = context.getSoftwarePath().resolve(tool); + context.getFileAccess().symlink(pathToInstallationOfDummyTool, pathToLinkedSoftware); + } + + /** Test of {@link VersionGetCommandlet} run, when Installed Version is null. */ + @Test + public void testVersionGetCommandletRunPrintConfiguredEdition() { + + // arrange + String path = "workspaces/foo-test/my-git-repo"; + IdeTestContext context = newContext("basic", path, false); + EditionGetCommandlet editionGet = context.getCommandletManager().getCommandlet(EditionGetCommandlet.class); + editionGet.tool.setValueAsString("java", context); + // act + editionGet.run(); + // assert + assertLogMessage(context, IdeLogLevel.INFO, "The configured edition for tool java is java"); + assertLogMessage(context, IdeLogLevel.INFO, "To install that edition call the following command:"); + assertLogMessage(context, IdeLogLevel.INFO, "ide install java"); + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionListCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionListCommandletTest.java new file mode 100644 index 000000000..16bde2315 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionListCommandletTest.java @@ -0,0 +1,28 @@ +package com.devonfw.tools.ide.commandlet; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.log.IdeLogLevel; +import org.junit.jupiter.api.Test; + +/** Integration test of {@link EditionListCommandlet}. */ +public class EditionListCommandletTest extends AbstractIdeContextTest { + + /** Test of {@link EditionListCommandlet} run. */ + @Test + public void testEditionListCommandletRun() { + + // arrange + String path = "workspaces/foo-test/my-git-repo"; + IdeTestContext context = newContext("basic", path, false); + EditionListCommandlet editionList = context.getCommandletManager().getCommandlet(EditionListCommandlet.class); + editionList.tool.setValueAsString("mvn", context); + + // act + editionList.run(); + + // assert + assertLogMessage(context, IdeLogLevel.INFO, "mvn"); + assertLogMessage(context, IdeLogLevel.INFO, "secondMvnEdition"); + } +} \ No newline at end of file diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionSetCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionSetCommandletTest.java new file mode 100644 index 000000000..bc70f23ad --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionSetCommandletTest.java @@ -0,0 +1,58 @@ +package com.devonfw.tools.ide.commandlet; + +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.nio.file.Path; +import java.util.List; + +/** Integration test of {@link EditionSetCommandlet}. */ +public class EditionSetCommandletTest extends AbstractIdeContextTest { + + /** Test of {@link VersionSetCommandlet} run. */ + @Test + public void testEditionSetCommandletRun() { + + // arrange + String path = "workspaces/foo-test/my-git-repo"; + IdeContext context = newContext("basic", path, true); + EditionSetCommandlet editionSet = context.getCommandletManager().getCommandlet(EditionSetCommandlet.class); + editionSet.tool.setValueAsString("mvn", context); + editionSet.edition.setValueAsString("setEdition", context); + + // act + editionSet.run(); + + // assert + List logs = ((IdeTestContext) context).level(IdeLogLevel.WARNING).getMessages(); + assertThat(logs).containsExactly("Edition setEdition seems to be invalid"); + Path settingsIdeProperties = context.getSettingsPath().resolve("ide.properties"); + assertThat(settingsIdeProperties).hasContent(""" + #******************************************************************************** + # This file contains project specific environment variables + #******************************************************************************** + + JAVA_VERSION=17* + MVN_VERSION=3.9.* + ECLIPSE_VERSION=2023-03 + INTELLIJ_EDITION=ultimate + + IDE_TOOLS=mvn,eclipse + + BAR=bar-${SOME} + + TEST_ARGS1=${TEST_ARGS1} settings1 + TEST_ARGS4=${TEST_ARGS4} settings4 + TEST_ARGS5=${TEST_ARGS5} settings5 + TEST_ARGS6=${TEST_ARGS6} settings6 + TEST_ARGS7=${TEST_ARGS7} settings7 + TEST_ARGS8=settings8 + TEST_ARGS9=settings9 + TEST_ARGSb=${TEST_ARGS10} settingsb ${TEST_ARGSa} ${TEST_ARGSb} + TEST_ARGSc=${TEST_ARGSc} settingsc + MVN_EDITION=setEdition"""); + } +} \ No newline at end of file diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java index 9b2a9681d..ac21f6f86 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -26,7 +25,7 @@ public class InstallCommandletTest extends AbstractIdeContextTest { private static WireMockServer server; - private static Path resourcePath = Paths.get("src/test/resources"); + private static Path resourcePath = Path.of("src/test/resources"); @BeforeAll static void setUp() throws IOException { @@ -103,7 +102,7 @@ private void assertTestInstall(IdeContext context) { assertThat(context.getSoftwarePath().resolve("java")).exists(); assertThat(context.getSoftwarePath().resolve("java/InstallTest.txt")).hasContent("This is a test file."); assertThat(context.getSoftwarePath().resolve("java/bin/HelloWorld.txt")).hasContent("Hello World!"); - if(context.getSystemInfo().isWindows()){ + if (context.getSystemInfo().isWindows()) { assertThat(context.getSoftwarePath().resolve("java/bin/java.cmd")).exists(); } else if (context.getSystemInfo().isLinux() || context.getSystemInfo().isMac()) { assertThat(context.getSoftwarePath().resolve("java/bin/java")).exists(); diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionGetCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionGetCommandletTest.java index c03c68eb8..ffd46dc38 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionGetCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionGetCommandletTest.java @@ -17,7 +17,7 @@ public class VersionGetCommandletTest extends AbstractIdeContextTest { * Test of {@link VersionGetCommandlet} run, when Installed Version is null. */ @Test - public void testVersionGetCommandletRunThrowsCliExeption() { + public void testVersionGetCommandletRunThrowsCliException() { // arrange String path = "workspaces/foo-test/my-git-repo"; diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionSetCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionSetCommandletTest.java index 61d817c83..fdfd493d6 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionSetCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionSetCommandletTest.java @@ -44,7 +44,7 @@ public void testVersionSetCommandletRun() throws IOException { IDE_TOOLS=mvn,eclipse BAR=bar-${SOME} - + TEST_ARGS1=${TEST_ARGS1} settings1 TEST_ARGS4=${TEST_ARGS4} settings4 TEST_ARGS5=${TEST_ARGS5} settings5 diff --git a/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java b/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java new file mode 100644 index 000000000..e2c0a8e23 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java @@ -0,0 +1,138 @@ +package com.devonfw.tools.ide.completion; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.cli.AutocompletionReaderTestSupport; +import com.devonfw.tools.ide.context.IdeTestContext; + +/** + * Integration test of {@link IdeCompleter}. + */ +public class IdeCompleterTest extends AutocompletionReaderTestSupport { + + /** + * Test of 1st level auto-completion (commandlet name). As suggestions are sorted alphabetically "helm" will be the + * first match. + */ + @Test + public void testIdeCompleterHelp() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("helm", new TestBuffer("he").tab().tab()); + } + + /** + * Test of 1st level auto-completion (commandlet name). Here we test the special case of the + * {@link com.devonfw.tools.ide.commandlet.VersionCommandlet} that has a long-option style. + */ + @Test + public void testIdeCompleterVersion() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("--version ", new TestBuffer("--vers").tab()); + } + + /** + * Test of 2nd level auto-completion with tool property of {@link com.devonfw.tools.ide.commandlet.InstallCommandlet}. + */ + @Test + public void testIdeCompleterInstall() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("install mvn ", new TestBuffer("install m").tab()); + } + + /** + * Test of 2nd level auto-completion with commandlet property of + * {@link com.devonfw.tools.ide.commandlet.HelpCommandlet}. + */ + @Test + public void testIdeCompleterHelpWithToolCompletion() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("help mvn ", new TestBuffer("help m").tab().tab()); + } + + /** + * Test of second option completion that is already present as short-option. + */ + @Test + public void testIdeCompleterDuplicatedOptions() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("-t --t", new TestBuffer("-t --t").tab()); + + } + + /** + * Test of 3rd level completion using version property of {@link com.devonfw.tools.ide.commandlet.InstallCommandlet} + * contextual to the specified tool. The version "3.2.1" is the latest one from the mocked "basic" project configured + * for the tool "mvn". + */ + @Test + public void testIdeCompleterThirdLayerVersions() { + + String path = "workspaces/foo-test/my-git-repo"; + IdeTestContext ideContext = newContext("basic", path, false); + this.reader.setCompleter(new IdeCompleter(ideContext)); + assertBuffer("install mvn 3.2.1", new TestBuffer("install mvn ").tab().tab()); + } + + /** + * Test that 2nd level completion of undefined commandlet has no effect. + */ + @Test + public void testIdeCompleterNonExistentCommand() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("cd ", new TestBuffer("cd ").tab().tab().tab()); + + } + + /** + * Test that no options are completed on 2nd level for {@link com.devonfw.tools.ide.commandlet.VersionGetCommandlet} + * that has no options. + */ + @Test + public void testIdeCompleterPreventsOptionsAfterCommandWithMinus() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("get-version -", new TestBuffer("get-version -").tab().tab()); + assertBuffer("get-version - ", new TestBuffer("get-version - ").tab().tab()); + + } + + /** + * Test that completion with invalid options does not trigger suggestions. + */ + @Test + public void testIdeCompleterWithInvalidInputDoesNothing() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("get-version -t ", new TestBuffer("get-version -t ").tab().tab()); + assertBuffer("- get-version ", new TestBuffer("- get-version ").tab().tab()); + assertBuffer(" - get-version", new TestBuffer(" - get-version").tab().tab()); + } + + /** + * Test of 2nd level completion of tool property for {@link com.devonfw.tools.ide.commandlet.VersionGetCommandlet}. + */ + @Test + public void testIdeCompleterHandlesOptionsBeforeCommand() { + + this.reader.setCompleter(newCompleter()); + assertBuffer("get-version mvn ", new TestBuffer("get-version mv").tab().tab()); + } + + private IdeCompleter newCompleter() { + + return new IdeCompleter(newTestContext()); + } + + private IdeTestContext newTestContext() { + + return new IdeTestContext(Path.of(""), ""); + } +} 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 1cf0cab57..966e16374 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,7 +1,6 @@ package com.devonfw.tools.ide.context; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import org.assertj.core.api.Assertions; @@ -24,10 +23,10 @@ public abstract class AbstractIdeContextTest extends Assertions { protected static final String PROJECT_BASIC = "basic"; /** The source {@link Path} to the test projects. */ - protected static final Path PATH_PROJECTS = Paths.get("src/test/resources/ide-projects"); + protected static final Path PATH_PROJECTS = Path.of("src/test/resources/ide-projects"); // will not use eclipse-target like done in maven via eclipse profile... - private static final Path PATH_PROJECTS_COPY = Paths.get("target/test-projects/"); + private static final Path PATH_PROJECTS_COPY = Path.of("target/test-projects/"); /** Chunk size to use for progress bars **/ private static final int CHUNK_SIZE = 1024; @@ -118,7 +117,7 @@ protected static void assertLogMessage(IdeTestContext context, IdeLogLevel level public boolean matches(String e) { return e.contains(message); - }; + } }; assertion.filteredOn(condition).isNotEmpty(); } else { diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java index d47ab227f..9810990e9 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java @@ -55,7 +55,7 @@ protected String readLine() { */ public Map getProgressBarMap() { - return progressBarMap; + return this.progressBarMap; } @Override @@ -63,7 +63,10 @@ public IdeProgressBar prepareProgressBar(String taskName, long size) { IdeProgressBarTestImpl progressBar = new IdeProgressBarTestImpl(taskName, size); IdeProgressBarTestImpl duplicate = this.progressBarMap.put(taskName, progressBar); - assert duplicate == null; + // If we have multiple downloads, we may have an existing "Downloading" key + if (!taskName.equals("Downloading")) { + assert duplicate == null; + } return progressBar; } diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContext.java b/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContext.java index afd4c321a..1ad4ee8bd 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContext.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContext.java @@ -1,7 +1,6 @@ package com.devonfw.tools.ide.context; import java.nio.file.Path; -import java.nio.file.Paths; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.log.IdeTestLogger; @@ -33,7 +32,7 @@ public IdeTestLogger level(IdeLogLevel level) { */ public static IdeTestContext of() { - return new IdeTestContext(Paths.get("/")); + return new IdeTestContext(Path.of("/")); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContextMock.java index 2bb868220..ec97dd45d 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContextMock.java @@ -1,6 +1,6 @@ package com.devonfw.tools.ide.context; -import java.nio.file.Paths; +import java.nio.file.Path; /** * Mock instance of {@link com.devonfw.tools.ide.context.IdeContext}. @@ -13,7 +13,7 @@ public class IdeTestContextMock extends IdeSlf4jContext { private IdeTestContextMock() { - super(Paths.get("/")); + super(Path.of("/")); } @Override diff --git a/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFileTest.java b/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFileTest.java index c7e0bbddb..7a8198b10 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFileTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFileTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; import com.devonfw.tools.ide.context.IdeTestContextMock; +import org.junit.jupiter.api.io.TempDir; /** * Test of {@link EnvironmentVariablesPropertiesFile}. @@ -40,7 +41,7 @@ public void testLoad() { } @Test - void testSave() throws Exception { + void testSave(@TempDir Path tempDir) throws Exception { // arrange List linesToWrite = new ArrayList<>(); @@ -60,7 +61,7 @@ void testSave() throws Exception { linesToWrite.add("# 5th comment"); linesToWrite.add("var9=9"); - Path propertiesFilePath = Path.of("target/tmp-EnvironmentVariablesPropertiesFileTest-ide.properties"); + Path propertiesFilePath = tempDir.resolve("test.properties"); Files.write(propertiesFilePath, linesToWrite, StandardOpenOption.CREATE_NEW); // check if this writing was correct List lines = Files.readAllLines(propertiesFilePath); @@ -76,7 +77,7 @@ void testSave() throws Exception { variables.set("var5", "5", true); variables.set("var1", "1.0", false); variables.set("var10", "10", false); - variables.set("var11", "11", true); // var11 must be set after var 10, the other lines can be shuffled + variables.set("var11", "11", true); variables.set("var3", "3", false); variables.set("var7", "7", true); variables.set("var6", "6.0", true); @@ -107,7 +108,5 @@ void testSave() throws Exception { lines = Files.readAllLines(propertiesFilePath); assertThat(lines).containsExactlyElementsOf(linesAfterSave); - // clean up - Files.delete(propertiesFilePath); } } \ No newline at end of file diff --git a/cli/src/test/java/com/devonfw/tools/ide/environment/SortedPropertiesTest.java b/cli/src/test/java/com/devonfw/tools/ide/environment/SortedPropertiesTest.java index 5a6fba48a..a821be3d8 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/environment/SortedPropertiesTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/environment/SortedPropertiesTest.java @@ -6,8 +6,6 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import com.devonfw.tools.ide.environment.SortedProperties; - /** * Test of {@link SortedProperties}. */ diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java index c2f0bae8c..33f2b7183 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.io; +import static com.devonfw.tools.ide.io.FileAccessImpl.generatePermissionString; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; @@ -7,6 +8,9 @@ import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -325,7 +329,7 @@ private void createSymlinks(FileAccess fa, Path dir, boolean relative) { /** * Checks if the symlinks exist. This is used by the tests of {@link FileAccessImpl#symlink(Path, Path, boolean)}. - * + * * @param dir the {@link Path} to the directory where the symlinks are expected. */ private void assertSymlinksExist(Path dir) { @@ -469,4 +473,99 @@ private void assertSymlinkRead(Path link, Path trueTarget) { + " and readPath " + readPath, e); } } + + /** + * Test of {@link FileAccessImpl#untar(Path, Path, TarCompression)} with {@link TarCompression#NONE} and checks if + * file permissions are preserved on Unix. + */ + @Test + public void testUntarWithNoneCompressionWithFilePermissions(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (context.getSystemInfo().isWindows()) { + return; + } + + // act + context.getFileAccess().untar( + Path.of("src/test/resources/com/devonfw/tools/ide/io").resolve("executable_and_non_executable.tar"), tempDir, + TarCompression.NONE); + + // assert + assertPosixFilePermissions(tempDir.resolve("executableFile.txt"), "rwxrwxr-x"); + assertPosixFilePermissions(tempDir.resolve("nonExecutableFile.txt"), "rw-rw-r--"); + } + + /** + * Test of {@link FileAccessImpl#untar(Path, Path, TarCompression)} with {@link TarCompression#GZ} and checks if file + * permissions are preserved on Unix. + */ + @Test + public void testUntarWithGzCompressionWithFilePermissions(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (context.getSystemInfo().isWindows()) { + return; + } + + // act + context.getFileAccess().untar( + Path.of("src/test/resources/com/devonfw/tools/ide/io").resolve("executable_and_non_executable.tar.gz"), tempDir, + TarCompression.GZ); + + // assert + assertPosixFilePermissions(tempDir.resolve("executableFile.txt"), "rwxrwxr-x"); + assertPosixFilePermissions(tempDir.resolve("nonExecutableFile.txt"), "rw-rw-r--"); + } + + /** + * Test of {@link FileAccessImpl#untar(Path, Path, TarCompression)} with {@link TarCompression#BZIP2} and checks if + * file permissions are preserved on Unix. + */ + @Test + public void testUntarWithBzip2CompressionWithFilePermissions(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (context.getSystemInfo().isWindows()) { + return; + } + + // act + context.getFileAccess().untar( + Path.of("src/test/resources/com/devonfw/tools/ide/io").resolve("executable_and_non_executable.tar.bz2"), + tempDir, TarCompression.BZIP2); + + // assert + assertPosixFilePermissions(tempDir.resolve("executableFile.txt"), "rwxrwxr-x"); + assertPosixFilePermissions(tempDir.resolve("nonExecutableFile.txt"), "rw-rw-r--"); + } + + private void assertPosixFilePermissions(Path file, String permissions) { + + try { + Set posixPermissions = Files.getPosixFilePermissions(file); + String permissionStr = PosixFilePermissions.toString(posixPermissions); + assertThat(permissions).isEqualTo(permissionStr); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Test of {@link FileAccessImpl#generatePermissionString(int)}. + */ + @Test + public void testGeneratePermissionString() { + + assertThat(generatePermissionString(0)).isEqualTo("---------"); + assertThat(generatePermissionString(436)).isEqualTo("rw-rw-r--"); + assertThat(generatePermissionString(948)).isEqualTo("rw-rw-r--"); + assertThat(generatePermissionString(509)).isEqualTo("rwxrwxr-x"); + assertThat(generatePermissionString(511)).isEqualTo("rwxrwxrwx"); + + } + } diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java index 585664d5c..fa3ed0178 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java @@ -22,7 +22,7 @@ public class IdeProgressBarTest extends AbstractIdeContextTest { /** * Tests if a download of a file with a valid content length was displaying an {@link IdeProgressBar} properly. - * + * * @param tempDir temporary directory to use. */ @Test diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java index bc7ba82a7..5f6dbe037 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java +++ b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java @@ -62,7 +62,7 @@ public void close() { */ public List getEventList() { - return eventList; + return this.eventList; } /** @@ -70,7 +70,7 @@ public List getEventList() { */ public long getMaxSize() { - return max; + return this.max; } /** @@ -100,7 +100,7 @@ public ProgressEvent(long stepSize) { */ public Instant getTimestamp() { - return timestamp; + return this.timestamp; } /** @@ -108,7 +108,7 @@ public Instant getTimestamp() { */ public long getStepSize() { - return stepSize; + return this.stepSize; } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java b/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java index 55b0d37ba..4f3bb2342 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java @@ -1,7 +1,6 @@ package com.devonfw.tools.ide.merge; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Map.Entry; import java.util.Properties; @@ -54,7 +53,7 @@ public void testConfigurator(@TempDir Path workspaceDir) throws Exception { // act IdeContext context = newContext(PROJECT_BASIC, null, false); DirectoryMerger merger = context.getWorkspaceMerger(); - Path templates = Paths.get("src/test/resources/templates"); + Path templates = Path.of("src/test/resources/templates"); Path setup = templates.resolve(IdeContext.FOLDER_SETUP); Path update = templates.resolve(IdeContext.FOLDER_UPDATE); merger.merge(setup, update, context.getVariables(), workspaceDir); diff --git a/cli/src/test/java/com/devonfw/tools/ide/os/MacOsHelperTest.java b/cli/src/test/java/com/devonfw/tools/ide/os/MacOsHelperTest.java index bf05ebadc..19ffb8237 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/os/MacOsHelperTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/os/MacOsHelperTest.java @@ -1,7 +1,6 @@ package com.devonfw.tools.ide.os; import java.nio.file.Path; -import java.nio.file.Paths; import org.junit.jupiter.api.Test; @@ -15,7 +14,7 @@ public class MacOsHelperTest extends AbstractIdeContextTest { private static final IdeContext CONTEXT = newContext("basic", "", false); - private static final Path APPS_DIR = Paths.get("src/test/resources/mac-apps"); + private static final Path APPS_DIR = Path.of("src/test/resources/mac-apps"); /** Test "java" structure. */ @Test diff --git a/cli/src/test/java/com/devonfw/tools/ide/os/SystemInformationImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/os/SystemInformationImplTest.java index 4c21e6ee9..e94a414f1 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/os/SystemInformationImplTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/os/SystemInformationImplTest.java @@ -3,11 +3,6 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import com.devonfw.tools.ide.os.OperatingSystem; -import com.devonfw.tools.ide.os.SystemArchitecture; -import com.devonfw.tools.ide.os.SystemInfo; -import com.devonfw.tools.ide.os.SystemInfoImpl; - /** * Test of {@link SystemInfoImpl}. */ diff --git a/cli/src/test/java/com/devonfw/tools/ide/os/SystemInformationMock.java b/cli/src/test/java/com/devonfw/tools/ide/os/SystemInformationMock.java index c44d21b97..c7546a4ec 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/os/SystemInformationMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/os/SystemInformationMock.java @@ -1,10 +1,5 @@ package com.devonfw.tools.ide.os; -import com.devonfw.tools.ide.os.OperatingSystem; -import com.devonfw.tools.ide.os.SystemArchitecture; -import com.devonfw.tools.ide.os.SystemInfo; -import com.devonfw.tools.ide.os.SystemInfoImpl; - /** * Mock instances of {@link SystemInfo} to test OS specific behavior independent of the current OS running the test. */ diff --git a/cli/src/test/java/com/devonfw/tools/ide/property/LocalePropertyTest.java b/cli/src/test/java/com/devonfw/tools/ide/property/LocalePropertyTest.java index c0d6df364..35500f50b 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/property/LocalePropertyTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/property/LocalePropertyTest.java @@ -2,12 +2,13 @@ import java.util.Locale; -import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import com.devonfw.tools.ide.commandlet.ContextCommandlet; +import com.devonfw.tools.ide.completion.CompletionCandidate; import com.devonfw.tools.ide.completion.CompletionCandidateCollector; +import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeTestContextMock; @@ -45,10 +46,9 @@ public void testCompletion() { CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(context); // act LocaleProperty property = new LocaleProperty("--locale", true, null); - boolean success = property.completeValue(input, context, new ContextCommandlet(), collector); + property.completeValue(input, context, new ContextCommandlet(), collector); // assert - assertThat(success).isTrue(); - assertThat(collector.getCandidates().stream().map(c -> c.text())).containsExactly(expectedCandidates); + assertThat(collector.getCandidates().stream().map(CompletionCandidate::text)).containsExactly(expectedCandidates); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/ExamplePluginBasedCommandlet.java b/cli/src/test/java/com/devonfw/tools/ide/tool/ExamplePluginBasedCommandlet.java index 6570ce6a7..d15758a86 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/ExamplePluginBasedCommandlet.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/ExamplePluginBasedCommandlet.java @@ -6,6 +6,9 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.tool.ide.PluginDescriptor; +/** + * Example implementation of {@link PluginBasedCommandlet} for testing. + */ public class ExamplePluginBasedCommandlet extends PluginBasedCommandlet { /** * The constructor. @@ -13,7 +16,7 @@ public class ExamplePluginBasedCommandlet extends PluginBasedCommandlet { * @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 ExamplePluginBasedCommandlet(IdeContext context, String tool, Set tags) { diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/PluginBasedCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/PluginBasedCommandletTest.java index f72ee5a35..19a48573f 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/PluginBasedCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/PluginBasedCommandletTest.java @@ -1,6 +1,6 @@ package com.devonfw.tools.ide.tool; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.Map; import java.util.Set; @@ -12,30 +12,34 @@ import com.devonfw.tools.ide.context.IdeTestContext; import com.devonfw.tools.ide.tool.ide.PluginDescriptor; +/** + * Test of {@link PluginBasedCommandlet}. + */ public class PluginBasedCommandletTest extends AbstractIdeContextTest { @Test - void testGetPluginsMap() { + void testGetPluginsMap() { + IdeTestContext context = newContext(PROJECT_BASIC, "", true); String tool = "eclipse"; Set tags = null; ExamplePluginBasedCommandlet pluginBasedCommandlet = new ExamplePluginBasedCommandlet(context, tool, tags); Map pluginsMap = pluginBasedCommandlet.getPluginsMap(); - assertNotNull(pluginsMap); + assertThat(pluginsMap).isNotNull(); - assertTrue(pluginsMap.containsKey("checkstyle")); - assertTrue(pluginsMap.containsKey("anyedit")); + assertThat(pluginsMap.containsKey("checkstyle")).isTrue(); + assertThat(pluginsMap.containsKey("anyedit")).isTrue(); PluginDescriptor plugin1 = pluginsMap.get("checkstyle"); assertNotNull(plugin1); - assertEquals("checkstyle", plugin1.getName()); + assertThat(plugin1.getName()).isEqualTo("checkstyle"); PluginDescriptor plugin2 = pluginsMap.get("anyedit"); assertNotNull(plugin2); - assertEquals("anyedit", plugin2.getName()); + assertThat(plugin2.getName()).isEqualTo("anyedit"); // Check if anyedit plugin has value "false" --> value from user directory - assertEquals(false, plugin2.isActive()); + assertThat(plugin2.isActive()).isFalse(); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/UrlUpdaterMock.java b/cli/src/test/java/com/devonfw/tools/ide/tool/UrlUpdaterMock.java index 62a0f616e..a82ecc268 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/UrlUpdaterMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/UrlUpdaterMock.java @@ -1,15 +1,14 @@ package com.devonfw.tools.ide.tool; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import com.devonfw.tools.ide.url.model.folder.UrlRepository; import com.devonfw.tools.ide.url.model.folder.UrlVersion; import com.devonfw.tools.ide.url.updater.AbstractUrlUpdater; -import com.devonfw.tools.ide.url.updater.JsonUrlUpdater; import com.devonfw.tools.ide.url.updater.UrlUpdater; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - /** * Test mock for {@link UrlUpdater} preparing multiple tool versions and distributions. */ diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/UrlUpdaterTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/UrlUpdaterTest.java index a9af864ae..6bfdf43cb 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/UrlUpdaterTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/UrlUpdaterTest.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Instant; import org.junit.jupiter.api.Test; @@ -84,7 +83,7 @@ public void testUrlUpdaterIsNotUpdatingWhenStatusManualIsTrue(@TempDir Path temp // act updater.update(urlRepository); - Path versionsPath = Paths.get(testdataRoot).resolve("mocked").resolve("mocked").resolve("1.0"); + Path versionsPath = Path.of(testdataRoot).resolve("mocked").resolve("mocked").resolve("1.0"); // assert assertThat(versionsPath.resolve("windows_x64.urls")).doesNotExist(); @@ -100,7 +99,6 @@ public void testUrlUpdaterIsNotUpdatingWhenStatusManualIsTrue(@TempDir Path temp * See: #1343 for reference. * * @param tempDir Temporary directory - * @throws IOException test fails */ @Test public void testUrlUpdaterStatusJsonRefreshBugStillExisting(@TempDir Path tempDir) { @@ -147,8 +145,7 @@ public void testUrlUpdaterStatusJsonRefreshBugStillExisting(@TempDir Path tempDi assertThat(errorCode).isEqualTo(404); assertThat(errorTimestamp).isAfter(successTimestamp); - stubFor( - any(urlMatching("/os/.*")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/plain"))); + stubFor(any(urlMatching("/os/.*")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/plain"))); // re-initialize UrlRepository for error timestamp UrlRepository urlRepositoryWithSuccess = UrlRepository.load(tempDir); @@ -181,8 +178,7 @@ public void testUrlUpdaterStatusJsonRefreshBugStillExisting(@TempDir Path tempDi public void testUrlUpdaterWithTextContentTypeWillNotCreateStatusJson(@TempDir Path tempDir) { // given - stubFor(any(urlMatching("/os/.*")) - .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/plain").withBody("aBody"))); + stubFor(any(urlMatching("/os/.*")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/plain").withBody("aBody"))); UrlRepository urlRepository = UrlRepository.load(tempDir); UrlUpdaterMockSingle updater = new UrlUpdaterMockSingle(); diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioJsonUrlUpdaterTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioJsonUrlUpdaterTest.java index f7291ce2f..8817dfa28 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioJsonUrlUpdaterTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioJsonUrlUpdaterTest.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -44,7 +43,7 @@ public void testJsonUrlUpdaterCreatesDownloadUrlsAndChecksums(@TempDir Path temp // given stubFor(get(urlMatching("/android-studio-releases-list.*")).willReturn(aResponse().withStatus(200) - .withBody(Files.readAllBytes(Paths.get(testdataRoot).resolve("android-version.json"))))); + .withBody(Files.readAllBytes(Path.of(testdataRoot).resolve("android-version.json"))))); stubFor(any(urlMatching("/edgedl/android/studio/ide-zips.*")) .willReturn(aResponse().withStatus(200).withBody("aBody"))); @@ -83,7 +82,7 @@ public void testJsonUrlUpdaterWithMissingDownloadsDoesNotCreateVersionFolder(@Te // given stubFor(get(urlMatching("/android-studio-releases-list.*")).willReturn(aResponse().withStatus(200) - .withBody(Files.readAllBytes(Paths.get(testdataRoot).resolve("android-version.json"))))); + .withBody(Files.readAllBytes(Path.of(testdataRoot).resolve("android-version.json"))))); stubFor(get(urlMatching("/edgedl/android/studio/ide-zips.*")).willReturn(aResponse().withStatus(404))); @@ -112,7 +111,7 @@ public void testJsonUrlUpdaterWithMissingChecksumGeneratesChecksum(@TempDir Path // given stubFor(get(urlMatching("/android-studio-releases-list.*")).willReturn(aResponse().withStatus(200) - .withBody(Files.readAllBytes(Paths.get(testdataRoot).resolve("android-version-without-checksum.json"))))); + .withBody(Files.readAllBytes(Path.of(testdataRoot).resolve("android-version-without-checksum.json"))))); stubFor(any(urlMatching("/edgedl/android/studio/ide-zips.*")) .willReturn(aResponse().withStatus(200).withBody("aBody"))); diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijJsonUrlUpdaterTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijJsonUrlUpdaterTest.java index 2f106da27..4b5aa1ba9 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijJsonUrlUpdaterTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijJsonUrlUpdaterTest.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -44,7 +43,7 @@ public void testIntellijJsonUrlUpdaterCreatesDownloadUrlsAndChecksums(@TempDir P // given stubFor(get(urlMatching("/products.*")).willReturn(aResponse().withStatus(200) - .withBody(Files.readAllBytes(Paths.get(TEST_DATA_ROOT).resolve("intellij-version.json"))))); + .withBody(Files.readAllBytes(Path.of(TEST_DATA_ROOT).resolve("intellij-version.json"))))); stubFor(any(urlMatching("/idea/idea.*")).willReturn(aResponse().withStatus(200).withBody("aBody"))); @@ -76,7 +75,7 @@ public void testIntellijJsonUrlUpdaterWithMissingDownloadsDoesNotCreateVersionFo // given stubFor(get(urlMatching("/products.*")).willReturn(aResponse().withStatus(200) - .withBody(Files.readAllBytes(Paths.get(TEST_DATA_ROOT).resolve("intellij-version.json"))))); + .withBody(Files.readAllBytes(Path.of(TEST_DATA_ROOT).resolve("intellij-version.json"))))); stubFor(any(urlMatching("/idea/idea.*")).willReturn(aResponse().withStatus(404))); @@ -105,7 +104,7 @@ public void testIntellijJsonUrlUpdaterWithMissingChecksumGeneratesChecksum(@Temp // given stubFor(get(urlMatching("/products.*")).willReturn(aResponse().withStatus(200) - .withBody(Files.readAllBytes(Paths.get(TEST_DATA_ROOT).resolve("intellij-version-withoutchecksum.json"))))); + .withBody(Files.readAllBytes(Path.of(TEST_DATA_ROOT).resolve("intellij-version-withoutchecksum.json"))))); stubFor(any(urlMatching("/idea/idea.*")).willReturn(aResponse().withStatus(200).withBody("aBody"))); diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/jmc/JmcTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/jmc/JmcTest.java new file mode 100644 index 000000000..59477d1de --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/jmc/JmcTest.java @@ -0,0 +1,115 @@ +package com.devonfw.tools.ide.tool.jmc; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.commandlet.InstallCommandlet; +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; + +/** + * Integration test of {@link com.devonfw.tools.ide.tool.jmc.Jmc}. + */ +public class JmcTest extends AbstractIdeContextTest { + + private static WireMockServer server; + + private static Path resourcePath = Path.of("src/test/resources"); + + @BeforeAll + static void setUp() throws IOException { + server = new WireMockServer(WireMockConfiguration.wireMockConfig().port(1112)); + server.start(); + } + + @AfterAll + static void tearDown() throws IOException { + + server.shutdownServer(); + } + + private void mockWebServer() throws IOException { + + String windowsFilenameJmc = "org.openjdk.jmc-8.3.0-win32.win32.x86_64.zip"; + String linuxFilenameJmc = "org.openjdk.jmc-8.3.0-linux.gtk.x86_64.tar.gz"; + String macOSFilenameJmc = "org.openjdk.jmc-8.3.0-macosx.cocoa.x86_64.tar.gz"; + String windowsFilenameJava = "java-17.0.6-windows-x64.zip"; + String linuxFilenameJava = "java-17.0.6-linux-x64.tgz"; + String resourceFilesDirName = "__files"; + + Path windowsFilePathJmc = resourcePath.resolve(resourceFilesDirName).resolve(windowsFilenameJmc); + String windowsLengthJmc = String.valueOf(Files.size(windowsFilePathJmc)); + + Path linuxFilePathJmc = resourcePath.resolve(resourceFilesDirName).resolve(linuxFilenameJmc); + String linuxLengthJmc = String.valueOf(Files.size(linuxFilePathJmc)); + + Path macOSFilePathJmc = resourcePath.resolve(resourceFilesDirName).resolve(macOSFilenameJmc); + String maxOSLengthJmc = String.valueOf(Files.size(macOSFilePathJmc)); + + Path windowsFilePathJava = resourcePath.resolve(resourceFilesDirName).resolve(windowsFilenameJava); + String windowsLengthJava = String.valueOf(Files.size(windowsFilePathJava)); + + Path linuxFilePathJava = resourcePath.resolve(resourceFilesDirName).resolve(linuxFilenameJava); + String linuxLengthJava = String.valueOf(Files.size(linuxFilePathJava)); + + setupMockServerResponse("/jmcTest/windows", "application/zip", windowsLengthJmc, windowsFilenameJmc); + setupMockServerResponse("/jmcTest/linux", "application/gz", linuxLengthJmc, linuxFilenameJmc); + setupMockServerResponse("/jmcTest/macOS", "application/gz", maxOSLengthJmc, macOSFilenameJmc); + setupMockServerResponse("/installTest/windows", "application/zip", windowsLengthJava, windowsFilenameJava); + setupMockServerResponse("/installTest/linux", "application/tgz", linuxLengthJava, linuxFilenameJava); + setupMockServerResponse("/installTest/macOS", "application/tgz", linuxLengthJava, linuxFilenameJava); + + } + + private void setupMockServerResponse(String testUrl, String contentType, String contentLength, String bodyFile) { + + server.stubFor(get(urlPathEqualTo(testUrl)).willReturn(aResponse().withHeader("Content-Type", contentType) + .withHeader("Content-Length", contentLength).withStatus(200).withBodyFile(bodyFile))); + } + + @Test + public void jmcPostInstallShouldMoveFilesIfRequired() throws IOException { + + // arrange + String path = "workspaces/foo-test/my-git-repo"; + IdeContext context = newContext("basic", path, true); + InstallCommandlet install = context.getCommandletManager().getCommandlet(InstallCommandlet.class); + install.tool.setValueAsString("jmc", context); + mockWebServer(); + // act + install.run(); + + // assert + assertThat(context.getSoftwarePath().resolve("jmc")).exists(); + assertThat(context.getSoftwarePath().resolve("jmc/InstallTest.txt")).hasContent("This is a test file."); + + if (context.getSystemInfo().isWindows()) { + assertThat(context.getSoftwarePath().resolve("jmc/jmc.cmd")).exists(); + } else if (context.getSystemInfo().isLinux()) { + assertThat(context.getSoftwarePath().resolve("jmc/jmc")).exists(); + } + + if (context.getSystemInfo().isWindows() || context.getSystemInfo().isLinux()) { + assertThat(context.getSoftwarePath().resolve("jmc/HelloWorld.txt")).hasContent("Hello World!"); + assertThat(context.getSoftwarePath().resolve("jmc/JDK Mission Control")).doesNotExist(); + } + + if (context.getSystemInfo().isMac()) { + assertThat(context.getSoftwarePath().resolve("jmc/JDK Mission Control.app")).exists(); + assertThat(context.getSoftwarePath().resolve("jmc/JDK Mission Control.app/Contents")).exists(); + } + + } + +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/python/PythonUrlUpdaterTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/python/PythonUrlUpdaterTest.java index f8615afbb..36c51c191 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/python/PythonUrlUpdaterTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/python/PythonUrlUpdaterTest.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -37,7 +36,7 @@ public void testPythonURl(@TempDir Path tempPath) throws IOException { // given stubFor(get(urlMatching("/actions/python-versions/main/.*")).willReturn(aResponse().withStatus(200) - .withBody(Files.readAllBytes(Paths.get(testdataRoot).resolve("python-version.json"))))); + .withBody(Files.readAllBytes(Path.of(testdataRoot).resolve("python-version.json"))))); stubFor(any(urlMatching("/actions/python-versions/releases/download.*")) .willReturn(aResponse().withStatus(200).withBody("aBody"))); diff --git a/cli/src/test/java/com/devonfw/tools/ide/url/model/UrlStatusFileTest.java b/cli/src/test/java/com/devonfw/tools/ide/url/model/UrlStatusFileTest.java index 5baaf497e..527cbd651 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/url/model/UrlStatusFileTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/url/model/UrlStatusFileTest.java @@ -1,6 +1,6 @@ package com.devonfw.tools.ide.url.model; -import java.nio.file.Paths; +import java.nio.file.Path; import java.time.Instant; import org.assertj.core.api.Assertions; @@ -27,7 +27,7 @@ public class UrlStatusFileTest extends Assertions { public void testReadJson() { // given - UrlRepository repo = UrlRepository.load(Paths.get("src/test/resources/urls")); + UrlRepository repo = UrlRepository.load(Path.of("src/test/resources/urls")); UrlTool tool = repo.getChild("docker"); UrlEdition edition = tool.getChild("rancher"); UrlVersion version = edition.getChild("1.6.2"); diff --git a/cli/src/test/resources/__files/org.openjdk.jmc-8.3.0-linux.gtk.x86_64.tar.gz b/cli/src/test/resources/__files/org.openjdk.jmc-8.3.0-linux.gtk.x86_64.tar.gz new file mode 100644 index 000000000..931b84713 Binary files /dev/null and b/cli/src/test/resources/__files/org.openjdk.jmc-8.3.0-linux.gtk.x86_64.tar.gz differ diff --git a/cli/src/test/resources/__files/org.openjdk.jmc-8.3.0-macosx.cocoa.x86_64.tar.gz b/cli/src/test/resources/__files/org.openjdk.jmc-8.3.0-macosx.cocoa.x86_64.tar.gz new file mode 100644 index 000000000..134e89a54 Binary files /dev/null and b/cli/src/test/resources/__files/org.openjdk.jmc-8.3.0-macosx.cocoa.x86_64.tar.gz differ diff --git a/cli/src/test/resources/__files/org.openjdk.jmc-8.3.0-win32.win32.x86_64.zip b/cli/src/test/resources/__files/org.openjdk.jmc-8.3.0-win32.win32.x86_64.zip new file mode 100644 index 000000000..f14f4103f Binary files /dev/null and b/cli/src/test/resources/__files/org.openjdk.jmc-8.3.0-win32.win32.x86_64.zip differ diff --git a/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar new file mode 100644 index 000000000..86c1e1761 Binary files /dev/null and b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar differ diff --git a/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.bz2 b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.bz2 new file mode 100644 index 000000000..25fd0d565 Binary files /dev/null and b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.bz2 differ diff --git a/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.gz b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.gz new file mode 100644 index 000000000..29e02dff2 Binary files /dev/null and b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.gz differ diff --git a/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.zip b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.zip new file mode 100644 index 000000000..855957b5b Binary files /dev/null and b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.zip differ diff --git a/cli/src/test/resources/ide-projects/_ide/software/default/az/az/testVersion/.ide.software.version b/cli/src/test/resources/ide-projects/_ide/software/default/az/az/testVersion/.ide.software.version new file mode 100644 index 000000000..62ec2f639 --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/software/default/az/az/testVersion/.ide.software.version @@ -0,0 +1 @@ +testVersion diff --git a/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/linux_x64.sha256 b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/linux_x64.sha256 new file mode 100644 index 000000000..ba966d272 --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/linux_x64.sha256 @@ -0,0 +1 @@ +cf666655da9bc097a7413af6cc5e9d930bc1f9267410613707f5e4aa724e3bf9 \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/linux_x64.urls b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/linux_x64.urls new file mode 100644 index 000000000..57920ab67 --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/linux_x64.urls @@ -0,0 +1 @@ +http://localhost:1112/jmcTest/linux \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/mac_x64.sha256 b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/mac_x64.sha256 new file mode 100644 index 000000000..628ce170c --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/mac_x64.sha256 @@ -0,0 +1 @@ +44036f764b9b3ac0e788499ab9f3746bfac47ed09f4c464423a582f5698cabc8 \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/mac_x64.urls b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/mac_x64.urls new file mode 100644 index 000000000..9d3d0a85c --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/mac_x64.urls @@ -0,0 +1 @@ +http://localhost:1112/jmcTest/macOS \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/status.json b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/status.json new file mode 100644 index 000000000..b58452d90 --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/status.json @@ -0,0 +1,20 @@ +{ + "manual" : true, + "urls" : { + "-680270697" : { + "success" : { + "timestamp" : "2023-04-28T16:27:32.819394600Z" + } + }, + "-896197542" : { + "success" : { + "timestamp" : "2023-04-28T16:27:47.658175400Z" + } + }, + "-310367019" : { + "success" : { + "timestamp" : "2023-04-28T16:28:02.221367500Z" + } + } + } +} \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/windows_x64.urls b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/windows_x64.urls new file mode 100644 index 000000000..520aaac8d --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/windows_x64.urls @@ -0,0 +1 @@ +http://localhost:1112/jmcTest/windows \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/windows_x64.urls.sha256 b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/windows_x64.urls.sha256 new file mode 100644 index 000000000..83cc5866b --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/jmc/jmc/8.3.0/windows_x64.urls.sha256 @@ -0,0 +1 @@ +5cbb836ceb159788f03aed5d2da9debb8fa269139dc0e1f6ffff671ac5367e6b \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/mvn/secondMvnEdition/.gitkeep b/cli/src/test/resources/ide-projects/_ide/urls/mvn/secondMvnEdition/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/documentation/IDEasy-usage.asciidoc b/documentation/IDEasy-usage.asciidoc index 95b182c73..3e49c261c 100644 --- a/documentation/IDEasy-usage.asciidoc +++ b/documentation/IDEasy-usage.asciidoc @@ -15,6 +15,7 @@ include::variables.asciidoc[leveloffset=2] include::cli.asciidoc[leveloffset=2] include::docker-desktop-alternative.asciidoc[leveloffset=3] +include::jmc.asciidoc[leveloffset=3] <<<< diff --git a/documentation/LICENSE.asciidoc b/documentation/LICENSE.asciidoc index 31bc2c3e5..49779cf71 100644 --- a/documentation/LICENSE.asciidoc +++ b/documentation/LICENSE.asciidoc @@ -48,6 +48,8 @@ The following table shows the components that may be used. The column `inclusion |https://ant.apache.org/[Apache Ant]|Optional|https://github.com/apache/ant/blob/master/LICENSE[ASL 2.0] |https://gradle.org/[Gradle] |Optional|https://github.com/gradle/gradle/blob/master/LICENSE[ASL 2.0] |https://jenkins.io/[Jenkins] |Optional|https://github.com/jenkinsci/jenkins/blob/master/LICENSE.txt[MIT] +|https://github.com/jline/jline3[JLine3] |Optional|https://github.com/jline/jline3/blob/master/LICENSE.txt[BSD-3] +|https://github.com/fusesource/jansi[jansi] |Optional|https://github.com/fusesource/jansi/blob/master/license.txt[ASL 2.0] |https://www.sonarsource.com/plans-and-pricing/community/[SonarQube (Community Edition)] |Optional|https://github.com/SonarSource/sonarqube/blob/master/LICENSE.txt[LGPL 3.0] |https://www.sonarlint.org/eclipse/[SonarLint] |Optional|https://github.com/SonarSource/sonarlint-eclipse/blob/master/LICENSE.txt[LGPL 3+] |https://github.com/devonfw/devon4j[devon4j] |Optional|https://github.com/devonfw/devon4j/blob/develop/LICENSE[ASL 2.0] @@ -2654,6 +2656,250 @@ The externally maintained libraries used by Node.js are: SOFTWARE. """ + - JLine, located at cli, is licensed as follows: + """ + Copyright (c) 2002-2018, the original author or authors. + All rights reserved. + + https://opensource.org/licenses/BSD-3-Clause + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with + the distribution. + + Neither the name of JLine nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + """ + + - jansi, located at cli, is licensed as follows: + """ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + """ + - OWASP, located at security/security, is licensed as follows: """ Apache License diff --git a/documentation/coding-conventions.asciidoc b/documentation/coding-conventions.asciidoc new file mode 100644 index 000000000..c56156fdf --- /dev/null +++ b/documentation/coding-conventions.asciidoc @@ -0,0 +1,488 @@ +:toc: +toc::[] + += Coding Conventions + +The code should follow general conventions for Java (see http://www.oracle.com/technetwork/java/namingconventions-139351.html[Oracle Naming Conventions], https://google.github.io/styleguide/javaguide.html[Google Java Style], etc.). +We consider this as common sense instead of repeating this here. +The following sections give us additional conventions that we consider additionally. + +== Naming +We follow these additional naming rules: + +* Always use short but speaking names (for types, methods, fields, parameters, variables, constants, etc.). +* Avoid using existing type names from JDK (from `java.lang.*`, `java.util.*`, etc.) - so e.g. never name your own Java type `List`, `Error`, etc. +* Strictly avoid special characters in technical names (for files, types, fields, methods, properties, variables, database tables, columns, constraints, etc.). In other words only use Latin alpahnumeric ASCII characters with the common allowed technical separators for the accordign context (e.g. underscore) for technical names (even excluding whitespaces). +* For package segments and type names prefer singular forms (`CustomerEntity` instead of [line-through]`CustomersEntity`). Only use plural forms when there is no singular or it is really semantically required (e.g. for a container that contains multiple of such objects). +* Avoid having duplicate type names. The name of a class, interface, enum or annotation should be unique within your project unless this is intentionally desired in a special and reasonable situation. +* Avoid artificial naming constructs such as prefixes (`I*`) or suffixes (`*IF`) for interfaces. +* Use CamelCase even for abbreviations (`XmlUtil` instead of [line-through]`XMLUtil`) +* Avoid property/field names where the second character is upper-case at all (e.g. 'aBc'). See https://github.com/devonfw/cobigen/issues/1095[#1095] for details. +* Names of Generics should be easy to understand. Where suitable follow the common rule `E=Element`, `T=Type`, `K=Key`, `V=Value` but feel free to use longer names for more specific cases such as `ID`, `DTO` or `ENTITY`. The capitalized naming helps to distinguish a generic type from a regular class. +* For `boolean` getter methods use `is` prefix instead of `get` but for `boolean` variable names avoid the `is` prefix (`boolean force = ...` instead of `boolean isForce = ...`) unless the name is a reserved keyword (e.g. `boolean abstract = true` will result in a compile error so consider using `boolean isAbstract = true` instead). + +== Obsolete APIs +Please avoid using the following APIs: + +* `java.util.Date` - use according type from `java.time.*` such as `LocalDate`, `LocalDateTime`, or `Instant` +* `java.util.Calendar` - same as above +* `java.sql.Date` - use `LocalDate` +* `java.sql.Time` - use `LocalTime` +* `java.sql.Timestamp` - use `Instant` or `LocalDateTime` +* `java.io.File` - use `java.nio.file.Path` +* `java.nio.file.Paths.get(...)` - use `java.nio.file.Path.of(...)` +* `java.util.Vector` - use `List` and `ArrayList` or `LinkedList` +* `java.lang.StringBuffer` - use `java.lang.StringBuilder` +* `java.lang.Runtime.exec(...) - use `ProcessBuilder` (we use `ProcessContext` on top) +* `com.google.common.base.Objects` - use `java.util.Objects` + +== Code-Documentation +As a general goal, the code should be easy to read and understand. Besides, clear naming the documentation is important. We follow these rules: + +* APIs (especially component interfaces) are properly documented with JavaDoc. +* JavaDoc shall provide actual value - we do not write JavaDoc to satisfy tools such as checkstyle but to express information not already available in the signature. +* We make use of `{@link}` tags in JavaDoc to make it more expressive. +* JavaDoc of APIs describes how to use the type or method and not how the implementation internally works. +* To document implementation details, we use code comments (e.g. `// we have to flush explicitly to ensure version is up-to-date`). This is only needed for complex logic. +* Avoid the pointless `{@inheritDoc}` as since Java 1.5 there is the `@Override` annotation for overridden methods and your JavaDoc is inherited automatically even without any JavaDoc comment at all. + +== Catching and handling Exceptions +When catching exceptions always ensure the following: + +* Never call `printStackTrace()` method on an exception +* Either log or wrap and re-throw the entire catched exception. Be aware that the cause(s) of an exception is very valuable information. If you loose such information by improper exception-handling you may be unable to properly analyse production problems what can cause severe issues. +** If you wrap and re-throw an exception ensure that the catched exception is passed as cause to the newly created and thrown exception. +** If you log an exception ensure that the entire exception is passed as argument to the logger (and not only the result of `getMessage()` or `toString()` on the exception). + +[source,java] +---- +try { + doSomething(); +} catch (Exception e) { + // bad + throw new IllegalStateException("Something failed"); +} +---- + +This will result in a stacktrace like this: +[source,java] +---- +Exception in thread "main" java.lang.IllegalStateException: Something failed + at com.devonfw.tools.ide.ExceptionHandling.main(ExceptionHandling.java:14) +---- + +As you can see we have no information and clue what the catched `Exception` was and what really went wrong in `doSomething()`. + +Instead always rethrow with the original exception: +[source,java] +---- +try { + doSomething(); +} catch (Exception e) { + // good + throw new IllegalStateExeception("Something failed", e); +} +---- + +Now our stacktrace will look similar to this: +[source,java] +---- +Exception in thread "main" java.lang.IllegalStateException: Something failed + at com.devonfw.tools.ide.ExceptionHandling.main(ExceptionHandling.java:14) +Caused by: java.lang.IllegalArgumentException: Very important information + at com.devonfw.tools.ide.ExceptionHandling.doSomething(ExceptionHandling.java:23) + at com.devonfw.tools.ide.ExceptionHandling.main(ExceptionHandling.java:12) +---- + +Never do this severe mistake to lose this original exception cause! + +The same applies when logging the exception: +[source,java] +---- +try { + doSomething(); +} catch (Exception e) { + // bad + LOG.error("Something failed: " + e.getMessage()); +} +---- + +Instead include the full exception and use your logger properly: +[source,java] +---- +try { + doSomething(); +} catch (Exception e) { + // good + LOG.error("Something failed: {}", e.getMessage(), e); +} +---- + +Also please add contextual information to the message for the logger or the new exception. +So instead of just saying "Something failed" a really good example could look like this: +[source,java] +---- +LOG.error("An unexpected error occurred whilst downloading the tool {} with edition {} and version {} from URL {}.", tool, edition, version, url, e); +---- + +=== Prefer general API +Avoid unnecessary strong bindings: + +* Do not bind your code to implementations such as `Vector` or `ArrayList` instead of `List` +* In APIs for input (=parameters) always consider to make little assumptions: +** prefer `Collection` over `List` or `Set` where the difference does not matter (e.g. only use `Set` when you require uniqueness or highly efficient `contains`) +** consider preferring `Collection` over `Collection` when `Foo` is an interface or super-class + +=== Prefer primitive types +In general prefer primitive types (`boolean`, `int`, `long`, ...) instead of corresponding boxed object types (`Boolean`, `Integer`, `Long`, ...). +Only use boxed object types, if you explicitly want to allow `null` as a value. +Typically you never want to use `Boolean` but instead use `boolean`. +[source,java] +---- +// bad +public Boolean isEmpty { + return size() == 0; +} +---- +Instead always use the primitive `boolean` type: +[source,java] +---- +// good +public boolean isEmpty { + return size() == 0; +} +---- + +== Constants +Literals and values used in multiple places that do not change, shall be defined as constants. +A constant in a Java class is a type variable declared with the modifiers `static final`. +In an interface, `public static final` can and should be omitted since it is there by default. +[source,java] +---- +public class MavenDownloader { + // bad + public String url = "https://repo1.maven.org/maven2/" + public void download(Dependency dependency) { + String downloadUrl = url + dependency.getGroupId() + "/" + dependency.getArtifactId() + "/" dependency.getVersion() + "/" + dependency.getArtifactId() + "-" + dependency.getVersion() + ".jar"; + download(downloadUrl); + } + public void download(String url) { ... } +} +---- +Here `url` is used as a constant however it is not declared as such. +Instead we should better do this: +[source,java] +---- +public class MavenDownloader { + // good + /** The base URL of the central maven repository. */ + public static final String REPOSITORY_URL = "https://repo1.maven.org/maven2/" + public void download(Dependency dependency) { + String artifactId = dependency.getArtifactId(); + String version = dependency.getVersion(); + String downloadUrl = REPOSITORY_URL + dependency.getGroupId().replace(".", "/") + "/" + artifactId + "/" + version + "/" + artifactId + "-" + version + ".jar"; + download(downloadUrl); + } + public void download(String url) { ... } +} +---- + +As stated above in case of an interface simply omit the modifiers: +[source,java] +---- +public interface MavenDownloader { + // good + /** The base URL of the central maven repository. */ + String REPOSITORY_URL = "https://repo1.maven.org/maven2/" + void download(Dependency dependency); + void download(String url); +} +---- + +So we conclude: + +* we want to use constants to define and reuse common immutable values. +* by giving the constant a reasonable name, we make our code reable +* following Java best-practices constants are named in `UPPER_CASE_WITH_UNDERSCORES` syntax +* by adding JavaDoc to the constant we give additional details what this value is about and good for. +* In classes we declare the constant with the visibility followed by the keywords `static final`. +* In interfaces, we omit all modifiers as they always default to `public static final` for type variables. + +== Optionals +With `Optional` you can wrap values to avoid a `NullPointerException` (NPE). +However, it is not a good code-style to use `Optional` for every parameter or result to express that it may be null. +For such case use JavaDoc (or consider `@Nullable` or even better instead annotate `@NotNull` where `null` is not acceptable). + +However, `Optional` can be used to prevent NPEs in fluent calls (due to the lack of the elvis operator): +[source,java] +---- +Long id; +id = fooCto.getBar().getBar().getId(); // may cause NPE +id = Optional.ofNullable(fooCto).map(FooCto::getBar).map(BarCto::getBar).map(BarEto::getId).orElse(null); // null-safe +---- + +== Avoid catching NPE + +Please avoid catching `NullPointerException`: +[source,java] +---- +// bad +try { + variable.getFoo().doSomething(); +} catch (NullPointerException e) { + LOG.warning("foo was null"); +} +---- + +Better explicitly check for `null`: +[source,java] +---- +// good +Foo foo = null; +if (variable != null) { + foo = variable.getFoo(); +} +if (foo == null) { + LOG.warning("foo was null"); +} else { + foo.doSomething(); +} +---- + +Please note that the term `Exception` is used for something exceptional. +Further creating an instance of an `Exception` or `Throable` in Java is expensive as the entire Strack has to be collected and copied into arrays, etc. causing significant overhead. +This should always be avoided in situations we can easily avoid with a simple `if` check. + +== Consider extractig local variable for multiple method calls + +Calling the same method (cascades) multiple times is redundant and reduces readability and performance: +[source,java] +---- +// bad +Candidate candidate; +if (variable.getFoo().getFirst().getSize() > variable.getFoo().getSecond().getSize()) { + candidate = variable.getFoo().getFirst(); +} else { + candidate = variable.getFoo().getSecond(); +} +---- + +The method `getFoo()` is used in 4 places and called 3 times. Maybe the method call is expensive? +[source,java] +---- +// good +Candidate candidate; +Foo foo = variable.getFoo(); +Candidate first = foo.getFirst(); +Candidate second = foo.getSecond(); +if (first.getSize() > second.getSize()) { + candidate = first; +} else { + candidate = second; +} +---- + +Please note that your IDE can automatically refactor your code extracting all occurrences of the same method call within the method body to a local variable. + +== Encoding +Encoding (esp. Unicode with combining characters and surrogates) is a complex topic. +Please study this topic if you have to deal with encodings and processing of special characters. +For the basics follow these recommendations: + +* Whenever possible prefer unicode (UTF-8 or better) as encoding. +* Do not cast from `byte` to `char` (unicode characters can be composed of multiple bytes, such cast may only work for ASCII characters) +* Never convert the case of a String using the default locale. E.g. if you do `"HI".toLowerCase()` and your system locale is Turkish, then the output will be "hı" instead of "hi", which can lead to wrong assumptions and serious problems. If you want to do a "universal" case conversion always explicitly use an according western locale (e.g. `toLowerCase(Locale.US)`). Consider using a helper class (see e.g. https://github.com/m-m-m/base/blob/master/core/src/main/java/io/github/mmm/base/text/CaseHelper.java[CaseHelper]) or create your own little static utility for that in your project. +* Write your code independent from the default encoding (system property `file.encoding`) - this will most likely differ in JUnit from production environment +** Always provide an encoding when you create a `String` from `byte[]`: `new String(bytes, encoding)` +** Always provide an encoding when you create a `Reader` or `Writer` : `new InputStreamReader(inStream, encoding)` + +== BLOBs +Avoid using `byte[]` for BLOBs as this will load them entirely into your memory. +This will cause performance issues or out of memory errors. +Instead, use streams when dealing with BLOBs (`InputStream`, `OutputStream`, `Reader`, `Writer`). + +== Stateless Programming +When implementing logic as components or _beans_, we strongly encourage stateless programming. +This is not about data objects (e.g. JavaBeans) that are stateful by design. +Instead this applies to things like `IdeContext` and all its related child-objects. +Such classes shall never be modified after initialization. +Methods called at runtime (after initialization) do not assign fields (member variables of your class) or mutate the object stored in a field. +This allows your component or bean to be stateless and thread-safe. +Therefore it can be initialized as a singleton so only one instance is created and shared accross all threads of the application. +Ideally all fields are declared `final` otherwise be careful not to change them dynamically (except for lazy-initializations). +Here is an example: +[source,java] +---- +public class GitHelperImpl implements GitHelper { + + // bad + private boolean force; + + @Overide + public void gitPullOrClone(boolean force, Path target, String gitUrl) { + this.force = force; + if (Files.isDirectory(target.resolve(".git"))) { + gitPull(target); + } else { + gitClone(target, gitUrl); + } + } + + private void gitClone(Path target, String gitUrl) { ... } + + private void gitPull(Path target) { ... } +} +---- + +As you can see in the `bad` code fields of the class are assigned at runtime. +Since IDEasy is not implementing a concurremt multi-user application this is not really critical. +However, it is best-practice to avoid this pattern and generally follow thread-safe programming as best-practice: +[source,java] +---- +public class GitHelperImpl implements GitHelper { + + // fine + @Overide + public void gitPullOrClone(boolean force, Path target, String gitUrl) { + if (Files.isDirectory(target.resolve(".git"))) { + gitPull(force, target); + } else { + gitClone(force, target, gitUrl); + } + } + + private void gitClone(boolean force, Path target, String gitUrl) { ... } + + private void gitPull(boolean force, Path target) { ... } +} +---- + +== Closing Resources +Resources such as streams (`InputStream`, `OutputStream`, `Reader`, `Writer`) or generally speaking implementations of `AutoClosable` need to be handled properly. +Therefore, it is important to follow these rules: + +* Each resource has to be closed properly, otherwise you will get out of file handles, TX sessions, memory leaks or the like. +* Where possible avoid to deal with such resources manually. +* In case you have to deal with resources manually (e.g. binary streams) ensure to close them properly via `try-with-resource` pattern. See the example below for details. + +Closing streams and other such resources is error prone. Have a look at the following example: +[source,java] +---- +// bad +try { + InputStream in = new FileInputStream(file); + readData(in); + in.close(); +} catch (IOException e) { + throw new IllegalStateException("Failed to read data.", e); +} +---- + +The code above is wrong as in case of an `IOException` the `InputStream` is not properly closed. +In a server application such mistakes can cause severe errors that typically will only occur in production. +As such resources implement the `AutoCloseable` interface you can use the `try-with-resource` syntax to write correct code. +The following code shows a correct version of the example: +[source,java] +---- +// fine +try (InputStream in = new FileInputStream(file)) { + readData(in); +} catch (IOException e) { + throw new IllegalStateException("Failed to read data.", e); +} +---- + +== Lambdas and Streams +With Java8 you have cool new features like lambdas and monads like (`Stream`, `CompletableFuture`, `Optional`, etc.). +However, these new features can also be misused or led to code that is hard to read or debug. To avoid pain, we give you the following best practices: + +. Learn how to use the new features properly before using. Developers are often keen on using cool new features. When you do your first experiments in your project code you will cause deep pain and might be ashamed afterwards. Please study the features properly. Even Java8 experts still write for loops to iterate over collections, so only use these features where it really makes sense. +. Streams shall only be used in fluent API calls as a Stream can not be forked or reused. +. Each stream has to have exactly one terminal operation. +. Do not write multiple statements into lambda code: ++ +[source,java] +---- +// bad +collection.stream().map(x -> { +Foo foo = doSomething(x); +... +return foo; +}).collect(Collectors.toList()); +---- ++ +This style makes the code hard to read and debug. Never do that! Instead, extract the lambda body to a private method with a meaningful name: ++ +[source,java] +---- +// fine +collection.stream().map(this::convertToFoo).collect(Collectors.toList()); +---- +. Do not use `parallelStream()` in general code (that will run on server side) unless you know exactly what you are doing and what is going on under the hood. Some developers might think that using parallel streams is a good idea as it will make the code faster. However, if you want to do performance optimizations talk to your technical lead (architect). Many features such as security and transactions will rely on contextual information that is associated with the current thread. Hence, using parallel streams will most probably cause serious bugs. Only use them for standalone (CLI) applications or for code that is just processing large amounts of data. +. Do not perform operations on a sub-stream inside a lambda: ++ +[source,java] +---- +set.stream().flatMap(x -> x.getChildren().stream().filter(this::isSpecial)).collect(Collectors.toList()); // bad +set.stream().flatMap(x -> x.getChildren().stream()).filter(this::isSpecial).collect(Collectors.toList()); // fine +---- +. Only use `collect` at the end of the stream: ++ +[source,java] +---- +set.stream().collect(Collectors.toList()).forEach(...) // bad +set.stream().peek(...).collect(Collectors.toList()) // fine +---- +. Lambda parameters with Types inference ++ +[source,java] +---- +(String a, Float b, Byte[] c) -> a.toString() + Float.toString(b) + Arrays.toString(c) // bad +(a,b,c) -> a.toString() + Float.toString(b) + Arrays.toString(c) // fine + +Collections.sort(personList, (Person p1, Person p2) -> p1.getSurName().compareTo(p2.getSurName())); // bad +Collections.sort(personList, (p1, p2) -> p1.getSurName().compareTo(p2.getSurName())); // fine +---- +. Avoid Return Braces and Statement ++ +[source,java] +---- + a -> { return a.toString(); } // bad + a -> a.toString(); // fine +---- +. Avoid Parentheses with Single Parameter ++ +[source,java] +---- +(a) -> a.toString(); // bad + a -> a.toString(); // fine +---- +. Avoid if/else inside foreach method. Use Filter method & comprehension ++ +[source,java] +---- +// bad +static public Iterator TwitterHandles(Iterator authors, string company) { + final List result = new ArrayList (); + foreach (Author a : authors) { + if (a.Company.equals(company)) { + String handle = a.TwitterHandle; + if (handle != null) + result.Add(handle); + } + } + return result; + } +---- ++ +[source,java] +---- +// fine +public List twitterHandles(List authors, String company) { + return authors.stream() + .filter(a -> null != a && a.getCompany().equals(company)) + .map(a -> a.getTwitterHandle()) + .collect(toList()); + } +---- + diff --git a/documentation/jmc.asciidoc b/documentation/jmc.asciidoc new file mode 100644 index 000000000..75f4918b6 --- /dev/null +++ b/documentation/jmc.asciidoc @@ -0,0 +1,14 @@ +:toc: +toc::[] + +# Java Mission Control + +The `jmc` commandlet allows to install and setup https://www.oracle.com/java/technologies/jdk-mission-control.html[Java Mission Control]. To learn more about Java Mission Control, please go https://docs.oracle.com/en/java/java-components/jdk-mission-control/index.html[here]. + +The arguments (`devon jmc «args»`) are explained by the following table: + +[options="header"] +|======================= +|*Command* |*Meaning* +|`install jmc` |install Java Mission Control (or update and verify) +|`jmc «args»` |run Java Mission Control with the given `«args»` \ No newline at end of file