From 489c8d6e6775d1f7be3e4b7dc591c12896f074d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Thu, 16 Nov 2023 17:25:10 +0100 Subject: [PATCH] #10: improve IDE support and configure workspace (#135) --- .../devonfw/tools/ide/cli/CliArgument.java | 33 ++ .../java/com/devonfw/tools/ide/cli/Ide.java | 2 +- .../tools/ide/commandlet/HelpCommandlet.java | 2 +- .../devonfw/tools/ide/configurator/Args.java | 80 ----- .../tools/ide/configurator/Configurator.java | 284 ------------------ .../configurator/merge/DirectoryMerger.java | 124 -------- .../configurator/merge/FallbackMerger.java | 43 --- .../ide/configurator/merge/FileMerger.java | 31 -- .../configurator/merge/FileTypeMerger.java | 29 -- .../configurator/merge/PropertiesMerger.java | 157 ---------- .../resolve/VariableResolver.java | 42 --- .../resolve/VariableResolverImpl.java | 138 --------- .../tools/ide/context/AbstractIdeContext.java | 14 +- .../devonfw/tools/ide/context/IdeContext.java | 34 +++ .../AbstractEnvironmentVariables.java | 111 +++++++ .../ide/environment/EnvironmentVariables.java | 28 ++ .../EnvironmentVariablesResolved.java | 68 +---- .../SortedProperties.java | 5 +- .../tools/ide/environment/VariableLine.java | 2 +- .../com/devonfw/tools/ide/io/FileAccess.java | 7 + .../devonfw/tools/ide/io/FileAccessImpl.java | 25 +- .../com/devonfw/tools/ide/logging/Log.java | 141 --------- .../com/devonfw/tools/ide/logging/Output.java | 120 -------- .../ide/merge/AbstractWorkspaceMerger.java | 46 +++ .../tools/ide/merge/DirectoryMerger.java | 137 +++++++++ .../tools/ide/merge/FallbackMerger.java | 55 ++++ .../devonfw/tools/ide/merge/FileMerger.java | 20 ++ .../{configurator => }/merge/JsonMerger.java | 125 ++++---- .../tools/ide/merge/PropertiesMerger.java | 160 ++++++++++ .../tools/ide/merge/WorkspaceMerger.java | 32 ++ .../{configurator => }/merge/XmlMerger.java | 90 +++--- .../tools/ide/tool/ToolCommandlet.java | 7 +- .../tools/ide/tool/eclipse/Eclipse.java | 44 ++- .../tools/ide/tool/ide/IdeToolCommandlet.java | 47 ++- .../tools/ide/tool/intellij/Intellij.java | 39 +++ .../tools/ide/variable/IdeVariables.java | 2 +- .../ide/variable/VariableDefinition.java | 4 + .../commandlet/VersionSetCommandletTest.java | 33 +- .../ide/configurator/ConfiguratorTest.java | 200 ------------ .../ide/context/AbstractIdeContextTest.java | 11 +- .../SortedPropertiesTest.java | 4 +- .../tools/ide/merge/DirectoryMergerTest.java | 166 ++++++++++ .../templates/setup/config/layout.xml | 2 +- .../templates/setup/json/settings.json | 4 +- .../templates/update/config/merge.xml | 2 +- .../templates/update/json/settings.json | 6 +- .../resources/templates/update/main.prefs | 2 +- 47 files changed, 1148 insertions(+), 1610 deletions(-) delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/configurator/Args.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/configurator/Configurator.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/configurator/merge/DirectoryMerger.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FallbackMerger.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FileMerger.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FileTypeMerger.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/configurator/merge/PropertiesMerger.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/configurator/resolve/VariableResolver.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/configurator/resolve/VariableResolverImpl.java rename cli/src/main/java/com/devonfw/tools/ide/{configurator => environment}/SortedProperties.java (96%) delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/logging/Log.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/logging/Output.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/merge/AbstractWorkspaceMerger.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/merge/DirectoryMerger.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/merge/FallbackMerger.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/merge/FileMerger.java rename cli/src/main/java/com/devonfw/tools/ide/{configurator => }/merge/JsonMerger.java (64%) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/merge/PropertiesMerger.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/merge/WorkspaceMerger.java rename cli/src/main/java/com/devonfw/tools/ide/{configurator => }/merge/XmlMerger.java (65%) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java delete mode 100644 cli/src/test/java/com/devonfw/tools/ide/configurator/ConfiguratorTest.java rename cli/src/test/java/com/devonfw/tools/ide/{configurator => environment}/SortedPropertiesTest.java (91%) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java index 049ea0876..b92801595 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliArgument.java @@ -213,4 +213,37 @@ public static CliArgument of(boolean splitShortOpt, String... args) { return first; } + /** + * @param firstArgs the first arguments. + * @param nextArgs the additional arguments to append after {@code args}. + * @return a {@link String} array with the values from {@code firstArgs} followed by the values from {@code nextArgs}. + */ + public static String[] append(String[] firstArgs, String... nextArgs) { + + return join(firstArgs, false, nextArgs); + } + + /** + * @param nextArgs the arguments to append after {@code firstArgs}. + * @param firstArgs the first arguments. + * @return a {@link String} array with the values from {@code firstArgs} followed by the values from {@code nextArgs}. + */ + public static String[] prepend(String[] nextArgs, String... firstArgs) { + + return join(nextArgs, false, firstArgs); + } + + private static String[] join(String[] args, boolean prefix, String... extraArgs) { + + String[] result = new String[args.length + extraArgs.length]; + int argsStart = 0; + int extraArgsStart = args.length; + if (prefix) { + argsStart = extraArgs.length; + extraArgsStart = 0; + } + System.arraycopy(args, 0, result, argsStart, args.length); + System.arraycopy(extraArgs, 0, result, extraArgsStart, extraArgs.length); + return result; + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/Ide.java b/cli/src/main/java/com/devonfw/tools/ide/cli/Ide.java index df6e040c4..e0fcf8ecc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/Ide.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/Ide.java @@ -63,7 +63,7 @@ public int run(String... args) { } catch (CliException error) { exitStatus = error.getExitCode(); if (context().level(IdeLogLevel.DEBUG).isEnabled()) { - context().error(error.getMessage(), error); + context().error(error, error.getMessage()); } else { context().error(error.getMessage()); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/HelpCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/HelpCommandlet.java index 83f07bdaa..49acf7aef 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/HelpCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/HelpCommandlet.java @@ -18,7 +18,7 @@ */ public final class HelpCommandlet extends Commandlet { - protected static final String LOGO = """ + static final String LOGO = """ __ ___ ___ ___ ╲ ╲ |_ _| ╲| __|__ _ ____ _ > > | || |) | _|/ _` (_-< || | diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/Args.java b/cli/src/main/java/com/devonfw/tools/ide/configurator/Args.java deleted file mode 100644 index 49c2e4492..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/Args.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.devonfw.tools.ide.configurator; - -import java.io.File; - -import com.devonfw.tools.ide.logging.Log; - -/** - * Simple container for the commandline arguments. - * - * @since 3.0.0 - */ -public class Args { - - private final String[] args; - - private int i; - - /** - * The constructor. - * - * @param args the arguments. - */ - public Args(String... args) { - - this.args = args; - this.i = 0; - } - - /** - * @return {@code true} if {@link #next() next argument} is available, {@code false} otherwise. - */ - public boolean hasNext() { - - return (this.i < this.args.length); - } - - /** - * @return the current argument. - */ - public String current() { - - if (this.i < this.args.length) { - return this.args[this.i]; - } - return null; - } - - /** - * @return the next argument. - * @see #hasNext() - */ - public String next() { - - if (this.i < this.args.length) { - return this.args[this.i++]; - } - return null; - } - - /** - * @param file the current {@link File} variable to check if the parameter was already applied. Initially - * {@code null}. - * @return the {@link #next() next argument} as {@link File}. - */ - public File nextFile(File file) { - - String arg = current(); - if (file != null) { - Log.warn("Duplicate option '" + arg + "'."); - } - File result = file; - if (this.i < this.args.length) { - result = new File(this.args[this.i++]); - } else { - Log.err("Option '" + arg + "' has to be followed by a file argument."); - } - return result; - } - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/Configurator.java b/cli/src/main/java/com/devonfw/tools/ide/configurator/Configurator.java deleted file mode 100644 index 5b506fcd0..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/Configurator.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.devonfw.tools.ide.configurator; - -import java.io.File; -import java.util.Locale; -import java.util.Properties; -import java.util.function.Supplier; - -import com.devonfw.tools.ide.configurator.merge.DirectoryMerger; -import com.devonfw.tools.ide.configurator.merge.PropertiesMerger; -import com.devonfw.tools.ide.configurator.resolve.VariableResolver; -import com.devonfw.tools.ide.configurator.resolve.VariableResolverImpl; -import com.devonfw.tools.ide.locking.EclipseWorkspaceLockChecker; -import com.devonfw.tools.ide.logging.Log; - -/** - * Class to create and update workspaces. - * - * @author trippl - */ -public class Configurator { - - /** - * The {@link java.io.File#getName() name} of the {@link java.io.File#isDirectory() folder} with the configuration - * templates for the initial setup of an workspace. - */ - public static final String FOLDER_SETUP = "setup"; - - /** - * The {@link java.io.File#getName() name} of the {@link java.io.File#isDirectory() folder} with the configuration - * templates for the update of an workspace. - */ - public static final String FOLDER_UPDATE = "update"; - - /** - * The systems file separator character. - */ - public static final String FILE_SEPARATOR = System.getProperty("file.separator"); - - /** The directory where java was executed from. */ - public static final String CURRENT_WORKING_DIRECTORY = cwd(); - - private static final String OPTION_VARIABLES = "-v"; - - private static final String OPTION_WORKSPACE = "-w"; - - private static final String OPTION_TEMPLATES = "-t"; - - private static final String OPTION_UPDATE = "-u"; - - private static final String OPTION_INVERSE = "-i"; - - private static final String OPTION_EXTEND = "-x"; - - private final DirectoryMerger merger; - - private VariableResolver resolver; - - private File workspaceFolder; - - private File setupFolder; - - private File updateFolder; - - /** - * The constructor. - */ - public Configurator() { - - this.merger = new DirectoryMerger(); - } - - private static String cwd() { - - String cwd = System.getProperty("user.dir"); - if (System.getProperty("os.name").toLowerCase(Locale.US).contains("win")) { - cwd = cwd.replace("\\", "/"); - } - return cwd; - } - - /** - * Creates or updates the workspace. - */ - private void createOrUpdateWorkspace() { - - this.merger.merge(this.setupFolder, this.updateFolder, this.resolver, this.workspaceFolder); - } - - /** - * Saves changes in the workspace files back into the update files. - * - * @param saveNewProperties - specifies if new properties are saved as well. - */ - private void saveChangesInWorkspace(boolean saveNewProperties) { - - this.merger.inverseMerge(this.workspaceFolder, this.resolver, saveNewProperties, this.updateFolder); - } - - /** - * Creates a {@link VariableResolverImpl} with the replacement patterns specified by the file at the - * replacementPatternsPath and the given regEx to find variables to resolve. - * - * @param variablesFile - path to the replacement patterns file. - * @return the created resolver. - */ - private VariableResolver createResolver(File variablesFile) { - - Properties variables = PropertiesMerger.loadIfExists(variablesFile); - putVariable(variables, VariableResolver.VARIABLE_DEVON_IDE_HOME, CURRENT_WORKING_DIRECTORY); - putVariable(variables, VariableResolver.VARIABLE_WORKSPACE_PATH, this.workspaceFolder.getPath()); - putSystemProperty(variables, "java.home"); - putEnvironmentVariable(variables, "JAVA_HOME", () -> CURRENT_WORKING_DIRECTORY + "/software/java"); - putEnvironmentVariable(variables, "ECLIPSE_HOME", () -> CURRENT_WORKING_DIRECTORY + "/software/eclipse"); - putEnvironmentVariable(variables, "SETTINGS_PATH", () -> CURRENT_WORKING_DIRECTORY + "/settings"); - return new VariableResolverImpl(variables); - } - - private static void putSystemProperty(Properties properties, String key) { - - putVariable(properties, key, System.getProperty(key)); - } - - private static void putEnvironmentVariable(Properties properties, String key, Supplier fallback) { - - String value = System.getenv(key); - if ((value == null) || value.isEmpty()) { - if (fallback != null) { - value = fallback.get(); - } - } - putVariable(properties, key, value); - } - - private static void putVariable(Properties properties, String key, String value) { - - if ((value != null) && !value.isEmpty()) { - if (value.startsWith("file:")) { - value = value.substring(5); - } - value = VariableResolverImpl.normalizePath(value); - properties.put(key, value); - Log.debug("Variable '" + key + "' = " + value); - } else { - Log.info("Variable '" + key + "' is undefined"); - } - } - - /** - * @see #main(String[]) - * @param args the command-line arguments. - * @return the {@link System#exit(int) exit-code}. - */ - public int run(String... args) { - - logCall(args); - File variablesFile = null; - File templatesFolder = null; - String command = null; - Args arguments = new Args(args); - while (arguments.hasNext()) { - String arg = arguments.next(); - if (OPTION_VARIABLES.equals(arg)) { - variablesFile = arguments.nextFile(variablesFile); - } else if (OPTION_WORKSPACE.equals(arg)) { - this.workspaceFolder = arguments.nextFile(this.workspaceFolder); - } else if (OPTION_TEMPLATES.equals(arg)) { - templatesFolder = arguments.nextFile(templatesFolder); - } else if (OPTION_UPDATE.equals(arg) || OPTION_INVERSE.equals(arg) || OPTION_EXTEND.equals(arg)) { - if (command != null) { - if (command.equals(arg)) { - Log.warn("Duplicate option '" + arg + "'."); - } else { - fail("Conflicting commands. Can not do both '" + command + "' and '" + arg + "'!"); - return -1; - } - } - command = arg; - } - } - if (!verifyFolder(this.workspaceFolder, "workspace")) { - return -1; - } else if (!verifyFolder(templatesFolder, "templates") || (templatesFolder == null)) { - return -1; - } else if (command == null) { - command = OPTION_UPDATE; - Log.warn("Missing command option. Using update (" + command + ") as fallback."); - } - try { - if (templatesFolder.getParentFile().getName().equals("eclipse")) { - File lockfile = new File(this.workspaceFolder, ".metadata/.lock"); - if (EclipseWorkspaceLockChecker.isLocked(lockfile)) { - System.err.println("Your workspace is locked at " + lockfile); - return 1; - } - } - this.resolver = createResolver(variablesFile); - this.setupFolder = new File(templatesFolder, FOLDER_SETUP); - this.updateFolder = new File(templatesFolder, FOLDER_UPDATE); - - File parentFile = templatesFolder.getParentFile(); - String ide = parentFile.getName(); - if ("workspace".equals(ide)) { - parentFile = parentFile.getParentFile(); - ide = parentFile.getName(); - } - String prefix = ide + "-"; - if (OPTION_UPDATE.equals(command)) { - Log.init(prefix + "update"); - Log.debug("Starting setup/update of workspace..."); - createOrUpdateWorkspace(); - } else if (OPTION_INVERSE.equals(command)) { - Log.init(prefix + "inverse-merge"); - Log.debug("Merging changes of workspace back to settings ..."); - saveChangesInWorkspace(false); - } else if (OPTION_EXTEND.equals(command)) { - Log.init(prefix + "inverse-merge-add"); - Log.debug("Merging changes of workspace back to settings (adding new properties)..."); - saveChangesInWorkspace(true); - } else { - throw new IllegalStateException(command); - } - return 0; - } catch (Exception e) { - Log.err("Configurator failed: " + e.getMessage(), e); - e.printStackTrace(); - return -1; - } - } - - private static boolean verifyFolder(File folder, String name) { - - if (folder == null) { - fail("No " + name + " folder configured."); - return false; - } else if (!folder.isDirectory()) { - fail("The " + name + " folder " + folder.getAbsolutePath() + " does not exist."); - return false; - } - return true; - } - - /** - * Runs the application. - * - * @param args the command-line arguments. - */ - public static void main(String[] args) { - - Configurator configurator = new Configurator(); - int exitCode = configurator.run(args); - System.exit(exitCode); - } - - private static void usage() { - - Log.info("USAGE: [-v ] -w -t -u|-i"); - Log.info( - " -v : specifies the properties file to use for replacements of variables in templates."); - Log.info(" -w : specifies the folder containing the workspace to manage."); - Log.info( - " -t : specifies the folder containing the templates to setup and update the workspace."); - Log.info(" -u: operation to create or update the workspace."); - Log.info( - " -i: operation to do the inverse logic and map back the workspace changes into the update templates."); - } - - private static void fail(String message) { - - Log.err(message); - usage(); - } - - private static void logCall(String[] args) { - - StringBuilder buffer = new StringBuilder(); - buffer.append(Configurator.class.getName()); - for (String arg : args) { - buffer.append(' '); - buffer.append(arg); - } - Log.debug(buffer.toString()); - } - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/DirectoryMerger.java b/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/DirectoryMerger.java deleted file mode 100644 index 073d0e815..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/DirectoryMerger.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.devonfw.tools.ide.configurator.merge; - -import java.io.File; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import com.devonfw.tools.ide.configurator.resolve.VariableResolver; -import com.devonfw.tools.ide.logging.Log; - -/** - * Implementation of {@link FileMerger} that does the whole thing: - *
    - *
  • It will recursively traverse directories.
  • - *
  • For each setup or update file if will delegate to the according {@link FileTypeMerger} based on the file - * extension.
  • - *
- * - * @since 3.0.0 - */ -public class DirectoryMerger implements FileMerger { - - private final Map extension2mergerMap; - - private final FallbackMerger fallbackMerger; - - /** - * The constructor. - */ - public DirectoryMerger() { - - super(); - this.extension2mergerMap = new HashMap<>(); - PropertiesMerger propertiesMerger = new PropertiesMerger(); - this.extension2mergerMap.put(".properties", propertiesMerger); - this.extension2mergerMap.put(".prefs", propertiesMerger); // Eclipse specific - XmlMerger xmlMerger = new XmlMerger(); - this.extension2mergerMap.put(".xml", xmlMerger); - this.extension2mergerMap.put(".xmi", xmlMerger); - this.extension2mergerMap.put(".launch", xmlMerger); // Eclipse specific - JsonMerger jsonMerger = new JsonMerger(); - this.extension2mergerMap.put(".json", jsonMerger); - this.fallbackMerger = new FallbackMerger(); - } - - @Override - public void merge(File setupFile, File updateFile, VariableResolver resolver, File workspaceFile) { - - String[] children = null; - if (setupFile.isDirectory()) { - children = setupFile.list(); - } - if (updateFile.isDirectory()) { - if (children == null) { - children = updateFile.list(); - } else { - Set set = new HashSet<>(); - addChildren(children, set); - addChildren(updateFile.list(), set); - children = set.toArray(children); - } - } - if (children == null) { - // file merge - FileTypeMerger merger = getMerger(workspaceFile); - merger.merge(setupFile, updateFile, resolver, workspaceFile); - } else { - // directory scan - for (String filename : children) { - merge(new File(setupFile, filename), new File(updateFile, filename), resolver, - new File(workspaceFile, filename)); - } - } - } - - private FileTypeMerger getMerger(File file) { - - String filename = file.getName(); - int lastDot = filename.lastIndexOf('.'); - if (lastDot > 0) { - String extension = filename.substring(lastDot); - Log.trace("Extension is " + extension); - FileTypeMerger merger = this.extension2mergerMap.get(extension); - if (merger != null) { - return merger; - } - } else { - Log.debug("No extension for " + file); - } - return this.fallbackMerger; - } - - @Override - public void inverseMerge(File workspaceFile, VariableResolver resolver, boolean addNewProperties, File updateFile) { - - if (updateFile.isDirectory()) { - if (!workspaceFile.isDirectory()) { - Log.warn("Workspace is missing directory: " + workspaceFile); - return; - } - Log.trace("Traversing directory " + updateFile); - for (File child : updateFile.listFiles()) { - inverseMerge(new File(workspaceFile, child.getName()), resolver, addNewProperties, - new File(updateFile, child.getName())); - } - } else if (workspaceFile.exists()) { - Log.debug("Start merging of changes from workspace back to file " + updateFile); - FileTypeMerger merger = getMerger(workspaceFile); - Log.trace("Using merger " + merger.getClass().getSimpleName()); - merger.inverseMerge(workspaceFile, resolver, addNewProperties, updateFile); - } else { - Log.warn("No such file or directory: " + updateFile); - } - } - - private void addChildren(String[] filenames, Set children) { - - for (String filename : filenames) { - children.add(filename); - } - } - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FallbackMerger.java b/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FallbackMerger.java deleted file mode 100644 index acd64061b..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FallbackMerger.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.devonfw.tools.ide.configurator.merge; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; - -import com.devonfw.tools.ide.configurator.resolve.VariableResolver; - -/** - * Implementation of {@link FileTypeMerger} to use as fallback. It can not actually merge but will simply overwrite the - * files. - * - * @since 3.0.0 - */ -public class FallbackMerger extends FileTypeMerger { - - @Override - public void merge(File setupFile, File updateFile, VariableResolver resolver, File workspaceFile) { - - if (updateFile.exists()) { - copy(updateFile, workspaceFile); - } else if (setupFile.exists() && !workspaceFile.exists()) { - copy(setupFile, workspaceFile); - } - } - - @Override - public void inverseMerge(File workspaceFile, VariableResolver resolver, boolean addNewProperties, File updateFile) { - - // nothing by default, we could copy the workspace file back to the update file if it exists... - } - - private void copy(File sourceFile, File targetFile) { - - ensureParentDirecotryExists(targetFile); - try { - Files.copy(sourceFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (Exception e) { - throw new IllegalStateException("Failed to copy file " + sourceFile + " to " + targetFile, e); - } - } - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FileMerger.java b/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FileMerger.java deleted file mode 100644 index 38ed02550..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FileMerger.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.devonfw.tools.ide.configurator.merge; - -import java.io.File; - -import com.devonfw.tools.ide.configurator.resolve.VariableResolver; - -/** - * Interface for a merger responsible for merging files. - * - * @since 3.0.0 - */ -public interface FileMerger { - - /** - * @param setupFile the setup {@link File} for creation. - * @param updateFile the update {@link File} for creation and update. - * @param resolver the {@link VariableResolver} to {@link VariableResolver#resolve(String) resolve variables}. - * @param workspaceFile the workspace {@link File} to create or update. - */ - void merge(File setupFile, File updateFile, VariableResolver resolver, File workspaceFile); - - /** - * @param workspaceFile the workspace {@link File} where to get the changes from. - * @param resolver the {@link VariableResolver} to {@link VariableResolver#inverseResolve(String) generalize variables}. - * @param addNewProperties - {@code true} to also add new properties to the {@code updateFile}, {@code false} - * otherwise (to only update existing properties). - * @param updateFile the update {@link File} - */ - void inverseMerge(File workspaceFile, VariableResolver resolver, boolean addNewProperties, File updateFile); - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FileTypeMerger.java b/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FileTypeMerger.java deleted file mode 100644 index 6cf3d5574..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/FileTypeMerger.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.devonfw.tools.ide.configurator.merge; - -import java.io.File; - -import com.devonfw.tools.ide.logging.Log; - -/** - * {@link FileMerger} responsible for a single type of {@link File}. - * - * @since 3.0.0 - */ -public abstract class FileTypeMerger implements FileMerger { - - /** - * @param file the {@link File} for which the {@link File#getParentFile() parent directory} needs to exists and will - * be created if absent by this method. - */ - protected static void ensureParentDirecotryExists(File file) { - - File parentDir = file.getParentFile(); - if (!parentDir.exists()) { - if (!parentDir.mkdirs()) { - Log.err("Could not create required directories for file: " + file.getAbsolutePath()); - return; - } - } - } - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/PropertiesMerger.java b/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/PropertiesMerger.java deleted file mode 100644 index 6b48af085..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/PropertiesMerger.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.devonfw.tools.ide.configurator.merge; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.util.Properties; -import java.util.Set; - -import com.devonfw.tools.ide.configurator.SortedProperties; -import com.devonfw.tools.ide.configurator.resolve.VariableResolver; -import com.devonfw.tools.ide.logging.Log; - -/** - * Implementation of {@link FileTypeMerger} for {@link Properties} files. - * - * @since 3.0.0 - */ -public class PropertiesMerger extends FileTypeMerger { - - @Override - public void merge(File setupFile, File updateFile, VariableResolver resolver, File workspaceFile) { - - SortedProperties properties = new SortedProperties(); - boolean updateFileExists = updateFile.exists(); - if (workspaceFile.exists()) { - if (!updateFileExists) { - Log.trace("Nothing to do as update file does not exist: " + updateFile); - return; // nothing to do ... - } - load(properties, workspaceFile); - } else if (setupFile.exists()) { - load(properties, setupFile); - } - if (updateFileExists) { - load(properties, updateFile); - } - resolve(properties, resolver); - save(properties, workspaceFile); - Log.trace("Saved merged properties to: " + workspaceFile); - } - - /** - * @param file the {@link File} to load. - * @return the loaded {@link Properties}. - */ - public static Properties load(File file) { - - Properties properties = new Properties(); - load(properties, file); - return properties; - } - - /** - * @param file the {@link File} to load. - * @return the loaded {@link Properties}. - */ - public static Properties loadIfExists(File file) { - - Properties properties = new Properties(); - if (file != null) { - if (file.exists()) { - load(properties, file); - } else { - Log.trace("Properties file does not exist: " + file); - } - } - return properties; - } - - /** - * @param properties the existing {@link Properties} instance. - * @param file the properties {@link File} to load. - */ - public static void load(Properties properties, File file) { - - Log.trace("Loading properties file " + file); - try (InputStream in = new FileInputStream(file); - Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { - properties.load(reader); - } catch (IOException e) { - throw new IllegalStateException("Could not load properties from file: " + file, e); - } - } - - private void resolve(Properties properties, VariableResolver resolver) { - - Set keys = properties.keySet(); - for (Object key : keys) { - String value = properties.getProperty(key.toString()); - properties.setProperty(key.toString(), resolver.resolve(value)); - } - } - - /** - * @param properties the {@link Properties} to save. - * @param file the {@link File} to save to. - */ - public static void save(Properties properties, File file) { - - Log.trace("Saving properties file " + file); - ensureParentDirecotryExists(file); - try (OutputStream out = new FileOutputStream(file); - Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { - properties.store(writer, null); - } catch (IOException e) { - throw new IllegalStateException("Could not write properties to file: " + file, e); - } - } - - @Override - public void inverseMerge(File workspaceFile, VariableResolver resolver, boolean addNewProperties, File updateFile) { - - if (!workspaceFile.exists()) { - Log.trace("Workspace file does not exist: " + workspaceFile.getAbsolutePath()); - return; - } - if (!updateFile.exists()) { - Log.trace("Update file does not exist: " + updateFile.getAbsolutePath()); - return; - } - Properties updateProperties = load(updateFile); - Properties workspaceProperties = load(workspaceFile); - SortedProperties mergedProperties = new SortedProperties(); - mergedProperties.putAll(updateProperties); - boolean updated = false; - for (Object key : workspaceProperties.keySet()) { - Object workspaceValue = workspaceProperties.get(key); - Object updateValue = updateProperties.get(key); - if ((updateValue != null) || addNewProperties) { - String updateValueResolved = null; - if (updateValue != null) { - updateValueResolved = resolver.resolve(updateValue.toString()); - } - if (!workspaceValue.equals(updateValueResolved)) { - String workspaceValueInverseResolved = resolver.inverseResolve(workspaceValue.toString()); - mergedProperties.put(key, workspaceValueInverseResolved); - updated = true; - } - } - } - if (updated) { - save(mergedProperties, updateFile); - Log.debug("Saved changes from " + workspaceFile.getName() + " to " + updateFile.getAbsolutePath()); - } else { - Log.trace("No changes for " + updateFile.getAbsolutePath()); - } - } - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/resolve/VariableResolver.java b/cli/src/main/java/com/devonfw/tools/ide/configurator/resolve/VariableResolver.java deleted file mode 100644 index 10b3e7c23..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/resolve/VariableResolver.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.devonfw.tools.ide.configurator.resolve; - -/** - * Interface for a resolver of variables. It is capable of replacing one or multiple occurrences of variables with their - * actual value. - * - * @see #resolve(String) - * - * @since 3.0.0 - */ -public interface VariableResolver { - - /** - * @deprecated Legacy that is only supported for compatibility. Please use {@link #VARIABLE_DEVON_IDE_HOME} instead. - */ - @Deprecated - String VARIABLE_CLIENT_ENV_HOME = "client.env.home"; - - /** Variable for top-level directory of 'devonfw-ide' installation (DEVON_IDE_HOME). */ - String VARIABLE_DEVON_IDE_HOME = "DEVON_IDE_HOME"; - - /** Variable for directory of the current workspace (WORKSPACE_PATH). */ - String VARIABLE_WORKSPACE_PATH = "WORKSPACE_PATH"; - - /** - * @param text the {@link String} to resolve (e.g. value of {@link java.util.Properties#get(Object) property} or from - * XML attribute or element). - * @return the resolved {@link String}. If the given {@link String} contained variables (e.g. "${«variable.name»}"), - * these are resolved and replaced with their actual value. - */ - String resolve(String text); - - /** - * Inverse operation of {@link #resolve(String)}. - * - * @param text the {@link String} to generalize (substitute values back to variable expressions). - * @return the generalized {@link String}. If the given {@link String} contained values of the variables (e.g. an - * absolute path), these are substituted back to variable expressions. - */ - String inverseResolve(String text); - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/resolve/VariableResolverImpl.java b/cli/src/main/java/com/devonfw/tools/ide/configurator/resolve/VariableResolverImpl.java deleted file mode 100644 index dd06c39c7..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/resolve/VariableResolverImpl.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.devonfw.tools.ide.configurator.resolve; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Map; -import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.devonfw.tools.ide.logging.Log; - -/** - * Implementation of {@link VariableResolver}. - * - * @since 3.0.0 - */ -public class VariableResolverImpl implements VariableResolver { - - private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}"); - - private static final String VARIABLE_PREFIX = "${"; - - private static final String VARIABLE_SUFFIX = "}"; - - private final Properties variables; - - private Properties altVariables; - - /** - * The constructor. - * - * @param variables the {@link Properties} with the variables to resolve. - */ - public VariableResolverImpl(Properties variables) { - - this.variables = variables; - } - - /** - * @param path a (potential) {@link Path} - * @return the given path without trailing slash. - */ - public static String normalizePath(String path) { - - if (path.endsWith("/") || path.endsWith("\\")) { - return path.substring(0, path.length() - 1); - } - return path; - } - - private Properties getAltVariables() { - - if (this.altVariables == null) { - this.altVariables = new Properties(); - for (Object keyObject : this.variables.keySet()) { - String key = keyObject.toString(); - String value = this.variables.getProperty(key); - Path path = Paths.get(value); - Log.trace("Checking if variable points to symlink: " + path); - if (Files.isSymbolicLink(path)) { - try { - Path resolved = Files.readSymbolicLink(path); - String altValue = normalizePath(resolved.toString()); - Log.debug("Symlink resolved to: " + altValue); - if (!altValue.equals(value)) { - this.altVariables.put(key, altValue); - } - } catch (IOException e) { - Log.debug("Ignoring error for resolving symlink: " + e.getMessage()); - } - } - } - } - return this.altVariables; - } - - @Override - public String resolve(String text) { - - Matcher m = VARIABLE_PATTERN.matcher(text); - String result = text; - StringBuffer sb = new StringBuffer(); - while (m.find()) { - String match = m.group(1); - String replacement = resolveVariable(match); - if (replacement != null) { - m.appendReplacement(sb, Matcher.quoteReplacement(replacement)); - } - } - m.appendTail(sb); - result = sb.toString(); - return result; - } - - /** - * @param name the name of the variable to resolve. - * @return the value of the variable with the given {@code name}. - */ - @SuppressWarnings("deprecation") - protected String resolveVariable(String name) { - - String result = null; - if (VariableResolver.VARIABLE_CLIENT_ENV_HOME.equals(name)) { - name = VariableResolver.VARIABLE_DEVON_IDE_HOME; - } - Object value = this.variables.get(name); - if (value != null) { - result = value.toString(); - } else { - result = System.getProperty(name); - if (result == null) { - result = System.getenv(name); - } - } - return result; - } - - @Override - public String inverseResolve(String text) { - - String result = text; - for (Map.Entry entry : this.variables.entrySet()) { - result = - result.replace(entry.getValue().toString(), VARIABLE_PREFIX + entry.getKey().toString() + VARIABLE_SUFFIX); - } - for (Map.Entry entry : getAltVariables().entrySet()) { - result = - result.replace(entry.getValue().toString(), VARIABLE_PREFIX + entry.getKey().toString() + VARIABLE_SUFFIX); - } - if (!result.equals(text)) { - Log.trace("Inverse resolved '" + text + "' to '" + result + "'."); - } - return result; - } - -} 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 6265878c6..941f7d025 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 @@ -23,6 +23,7 @@ import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.log.IdeSubLogger; import com.devonfw.tools.ide.log.IdeSubLoggerNone; +import com.devonfw.tools.ide.merge.DirectoryMerger; import com.devonfw.tools.ide.os.SystemInfo; import com.devonfw.tools.ide.os.SystemInfoImpl; import com.devonfw.tools.ide.process.ProcessContext; @@ -91,6 +92,8 @@ public abstract class AbstractIdeContext implements IdeContext { private final CustomToolRepository customToolRepository; + private final DirectoryMerger workspaceMerger; + private boolean offlineMode; private boolean forceMode; @@ -130,7 +133,7 @@ public AbstractIdeContext(IdeLogLevel minLogLevel, Function void addMapping(Map mapping, String key, O option) } } - - } diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index 7fe3ed6a9..0c8d0782f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -12,6 +12,7 @@ import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.io.IdeProgressBar; import com.devonfw.tools.ide.log.IdeLogger; +import com.devonfw.tools.ide.merge.DirectoryMerger; import com.devonfw.tools.ide.os.SystemInfo; import com.devonfw.tools.ide.process.ProcessContext; import com.devonfw.tools.ide.repo.CustomToolRepository; @@ -72,6 +73,33 @@ public interface IdeContext extends IdeLogger { */ String FOLDER_PLUGINS = "plugins"; + /** + * The name of the workspace folder inside the IDE specific {@link #FOLDER_SETTINGS settings} containing the + * configuration templates in #FOLDER_SETUP #FOLDER_UPDATE. + */ + String FOLDER_WORKSPACE = "workspace"; + + /** + * The name of the setup folder inside the {@link #FOLDER_WORKSPACE workspace} folder containing the templates for the + * configuration templates for the initial setup of a workspace. This is closely related with the + * {@link #FOLDER_UPDATE update} folder. + */ + String FOLDER_SETUP = "setup"; + + /** + * The name of the update folder inside the {@link #FOLDER_WORKSPACE workspace} folder containing the templates for + * the configuration templates for the update of a workspace. Configurations in this folder will be applied every time + * the IDE is started. They will override the settings the user may have manually configured every time. This is only + * for settings that have to be the same for every developer in the project. An example would be the number of spaces + * used for indentation and other code-formatting settings. If all developers in a project team use the same formatter + * settings, this will actively prevent diff-wars. However, the entire team needs to agree on these settings.
+ * Never configure aspects inside this update folder that may be of personal flavor such as the color theme. Otherwise + * developers will hate you as you actively take away their freedom to customize the IDE to their personal needs and + * wishes. Therefore do all "biased" or "flavored" configurations in {@link #FOLDER_SETUP setup} so these are only + * pre-configured but can be changed by the user as needed. + */ + String FOLDER_UPDATE = "update"; + /** The file where the installed software version is written to as plain text. */ String FILE_SOFTWARE_VERSION = ".ide.software.version"; @@ -358,4 +386,10 @@ default void requireOnline(String purpose) { */ IdeProgressBar prepareProgressBar(String taskName, long size); + /** + * @return the {@link DirectoryMerger} used to configure and merge the workspace for an + * {@link com.devonfw.tools.ide.tool.ide.IdeToolCommandlet IDE}. + */ + DirectoryMerger getWorkspaceMerger(); + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java index 98c8191ef..dad65e308 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java @@ -6,14 +6,34 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.variable.IdeVariables; +import com.devonfw.tools.ide.variable.VariableDefinition; /** * Abstract base implementation of {@link EnvironmentVariables}. */ public abstract class AbstractEnvironmentVariables implements EnvironmentVariables { + /** + * When we replace variable expressions with their value the resulting {@link String} can change in size (shrink or + * grow). By adding a bit of extra capacity we reduce the chance that the capacity is too small and a new buffer array + * has to be allocated and array-copy has to be performed. + */ + private static final int EXTRA_CAPACITY = 8; + + // Variable surrounded with "${" and "}" such as "${JAVA_HOME}" 1......2........ + private static final Pattern VARIABLE_SYNTAX = Pattern.compile("(\\$\\{([^}]+)})"); + + private static final int MAX_RECURSION = 9; + + private static final String VARIABLE_PREFIX = "${"; + + private static final String VARIABLE_SUFFIX = "}"; + /** @see #getParent() */ protected final AbstractEnvironmentVariables parent; @@ -138,6 +158,97 @@ public EnvironmentVariables resolved() { return new EnvironmentVariablesResolved(this); } + @Override + public String resolve(String string, Object src) { + + return resolve(string, src, 0, src, string); + } + + private String resolve(String value, Object src, int recursion, Object rootSrc, String rootValue) { + + if (value == null) { + return null; + } + if (recursion > MAX_RECURSION) { + throw new IllegalStateException("Reached maximum recursion resolving " + value + " for root valiable " + rootSrc + + " with value '" + rootValue + "'."); + } + recursion++; + Matcher matcher = VARIABLE_SYNTAX.matcher(value); + if (!matcher.find()) { + return value; + } + StringBuilder sb = new StringBuilder(value.length() + EXTRA_CAPACITY); + do { + String variableName = matcher.group(2); + String variableValue = getValue(variableName); + if (variableValue == null) { + this.context.warning("Undefined variable {} in '{}={}' for root '{}={}'", variableName, src, value, rootSrc, + rootValue); + } else { + String replacement = resolve(variableValue, variableName, recursion, rootSrc, rootValue); + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } + } while (matcher.find()); + matcher.appendTail(sb); + String resolved = sb.toString(); + return resolved; + } + + /** + * Like {@link #get(String)} but with higher-level features including to resolve {@link IdeVariables} with their + * default values. + * + * @param name the name of the variable to get. + * @return the value of the variable. + */ + protected String getValue(String name) { + + VariableDefinition var = IdeVariables.get(name); + String value; + if ((var != null) && var.isForceDefaultValue()) { + value = var.getDefaultValueAsString(this.context); + } else { + value = this.parent.get(name); + } + if ((value == null) && (var != null)) { + String key = var.getName(); + if (!name.equals(key)) { + value = this.parent.get(key); + } + if (value != null) { + value = var.getDefaultValueAsString(this.context); + } + } + if ((value != null) && (value.startsWith("~/"))) { + value = this.context.getUserHome() + value.substring(1); + } + return value; + } + + @Override + public String inverseResolve(String string, Object src) { + + String result = string; + // TODO add more variables to IdeVariables like JAVA_HOME + for (VariableDefinition variable : IdeVariables.VARIABLES) { + if (variable != IdeVariables.PATH) { + String name = variable.getName(); + String value = get(name); + if (value == null) { + value = variable.getDefaultValueAsString(this.context); + } + if (value != null) { + result = result.replace(value, VARIABLE_PREFIX + name + VARIABLE_SUFFIX); + } + } + } + if (!result.equals(string)) { + this.context.trace("Inverse resolved '{}' to '{}' from {}.", string, result, src); + } + return result; + } + @Override public String toString() { 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 b1ec691b2..5e5fc6aff 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 @@ -180,6 +180,34 @@ default EnvironmentVariables findVariable(String name) { */ Collection collectExportedVariables(); + /** + * @param string the {@link String} that potentially contains variables in the syntax "${«variable«}". Those will be + * resolved by this method and replaced with their {@link #get(String) value}. + * @param source the source where the {@link String} to resolve originates from. Should have a reasonable + * {@link Object#toString() string representation} that will be used in error or log messages if a variable + * could not be resolved. + * @return the the given {@link String} with the variables resolved. + * @see com.devonfw.tools.ide.tool.ide.IdeToolCommandlet + */ + String resolve(String string, Object source); + + /** + * The inverse operation of {@link #resolve(String, Object)}. Please note that the {@link #resolve(String, Object) + * resolve} operation is not fully bijective. There may be multiple variables holding the same {@link #get(String) + * value} or there may be static text that can be equal to a {@link #get(String) variable value}. This method does its + * best to implement the inverse resolution based on some heuristics. + * + * @param string the {@link String} where to find {@link #get(String) variable values} and replace them with according + * "${«variable«}" expressions. + * @param source the source where the {@link String} to inverse resolve originates from. Should have a reasonable + * {@link Object#toString() string representation} that will be used in error or log messages if the inverse + * resolving was not working as expected. + * @return the given {@link String} with {@link #get(String) variable values} replaced with according "${«variable«}" + * expressions. + * @see com.devonfw.tools.ide.tool.ide.IdeToolCommandlet + */ + String inverseResolve(String string, Object source); + /** * @param context the {@link IdeContext}. * @return the system {@link EnvironmentVariables} building the root of the {@link EnvironmentVariables} hierarchy. diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesResolved.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesResolved.java index 78e34b0dd..9a634a20c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesResolved.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesResolved.java @@ -1,21 +1,10 @@ package com.devonfw.tools.ide.environment; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.devonfw.tools.ide.variable.IdeVariables; -import com.devonfw.tools.ide.variable.VariableDefinition; - /** * Implementation of {@link EnvironmentVariables} that resolves variables recursively. */ public class EnvironmentVariablesResolved extends AbstractEnvironmentVariables { - // Variable surrounded with "${" and "}" such as "${JAVA_HOME}" 1......2........ - private static final Pattern VARIABLE_SYNTAX = Pattern.compile("(\\$\\{([^}]+)})"); - - private static final int MAX_RECURSION = 9; - /** * The constructor. * @@ -43,66 +32,11 @@ public String get(String name) { String value = getValue(name); if (value != null) { - value = resolve(value, name, 0, name, value); + value = resolve(value, name); } return value; } - private String getValue(String name) { - - VariableDefinition var = IdeVariables.get(name); - String value; - if ((var != null) && var.isForceDefaultValue()) { - value = var.getDefaultValueAsString(this.context); - } else { - value = this.parent.get(name); - } - if ((value == null) && (var != null)) { - String key = var.getName(); - if (!name.equals(key)) { - value = this.parent.get(key); - } - if (value != null) { - value = var.getDefaultValueAsString(this.context); - } - } - if ((value != null) && (value.startsWith("~/"))) { - value = this.context.getUserHome() + value.substring(1); - } - return value; - } - - String resolve(String value, String name, int recursion, String rootName, String rootValue) { - - if (value == null) { - return null; - } - if (recursion > MAX_RECURSION) { - throw new IllegalStateException("Reached maximum recursion resolving " + value + " for root valiable " + rootName - + " with value '" + rootValue + "'."); - } - recursion++; - Matcher matcher = VARIABLE_SYNTAX.matcher(value); - if (!matcher.find()) { - return value; - } - StringBuilder sb = new StringBuilder(value.length() + 8); - do { - String variableName = matcher.group(2); - String variableValue = getValue(variableName); - if (variableValue == null) { - this.context.warning("Undefined variable {} in '{}={}' for root '{}={}'", variableName, name, value, rootName, - rootValue); - } else { - String replacement = resolve(variableValue, variableName, recursion, rootName, rootValue); - matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); - } - } while (matcher.find()); - matcher.appendTail(sb); - String resolved = sb.toString(); - return resolved; - } - @Override public EnvironmentVariables resolved() { diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/SortedProperties.java b/cli/src/main/java/com/devonfw/tools/ide/environment/SortedProperties.java similarity index 96% rename from cli/src/main/java/com/devonfw/tools/ide/configurator/SortedProperties.java rename to cli/src/main/java/com/devonfw/tools/ide/environment/SortedProperties.java index f6a27b95b..6cdacb84a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/SortedProperties.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/SortedProperties.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.configurator; +package com.devonfw.tools.ide.environment; import java.util.ArrayList; import java.util.Collections; @@ -13,9 +13,6 @@ /** * {@link Properties} that are sorted ascending by their keys. - * - * @author trippl - * @since 3.0.0 */ public class SortedProperties extends Properties { diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/VariableLine.java b/cli/src/main/java/com/devonfw/tools/ide/environment/VariableLine.java index f8a7cb73c..28d2926d2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/VariableLine.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/VariableLine.java @@ -221,7 +221,7 @@ public String toString() { * Parses a {@link VariableLine} from {@link String}. * * @param line the {@link VariableLine} as {@link String} to parse. - * @param context the {@link IdeLogger}. + * @param logger the {@link IdeLogger}. * @param source the source where the given {@link String} to parse is from (e.g. the file path). * @return the parsed {@link VariableLine}. */ 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 82fe25090..8d2874471 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 @@ -32,6 +32,13 @@ public interface FileAccess { */ boolean isFile(Path file); + /** + * @param folder the {@link Path} to check. + * @return {@code true} if the given {@code folder} points to an existing directory, {@code false} otherwise (a + * warning is logged in this case). + */ + boolean isExpectedFolder(Path folder); + /** * @param file the {@link Path} to compute the checksum of. * @return the computed checksum (SHA-266). 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 3a916fcd1..331f80bb0 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 @@ -1,7 +1,5 @@ package com.devonfw.tools.ide.io; -import static com.devonfw.tools.ide.logging.Log.info; - import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -102,7 +100,7 @@ private void downloadFileWithProgressBar(String url, Path target, HttpResponse 0) { out.write(buf, 0, readBytes); pb.stepByOne(); @@ -184,6 +182,16 @@ public boolean isFile(Path file) { return true; } + @Override + public boolean isExpectedFolder(Path folder) { + + if (Files.isDirectory(folder)) { + return true; + } + this.context.warning("Expected folder was not found at {}", folder); + return false; + } + @Override public String checksum(Path file) { @@ -294,11 +302,10 @@ public void symlink(Path source, Path targetLink) { Files.createSymbolicLink(targetLink, source); } catch (FileSystemException e) { if (this.context.getSystemInfo().isWindows()) { - info( - "Due to lack of permissions, Microsofts mklink with junction had to be used to create a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for further details. Error was: " + this.context.info( + "Due to lack of permissions, Microsofts mklink with junction has to be used to create a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for further details. Error was: " + e.getMessage()); - - context.newProcess().executable("cmd") + this.context.newProcess().executable("cmd") .addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), source.toString()).run(); } else { throw new RuntimeException(e); diff --git a/cli/src/main/java/com/devonfw/tools/ide/logging/Log.java b/cli/src/main/java/com/devonfw/tools/ide/logging/Log.java deleted file mode 100644 index 1b5c59ed6..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/logging/Log.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.devonfw.tools.ide.logging; - -import java.io.File; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.logging.FileHandler; -import java.util.logging.Formatter; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogRecord; -import java.util.logging.Logger; - -import com.devonfw.tools.ide.configurator.Configurator; - -/** - * This class provides a {@link Logger} for logging purposes. To minimize dependencies we use the JDK default logger. - * - * @author trippl - * @since 3.0.0 - */ -public class Log { - - /** Logger instance. */ - private static final Logger LOGGER = Logger.getLogger(Configurator.class.getName()); - - /** - * @param filename the base filename of the logger. - */ - public static void init(String filename) { - - init(filename, false); - } - - /** - * @param filename the base filename of the logger. - * @param disableConsole - {@code true} to disable console logging, {@code false} otherwise. - */ - public static void init(String filename, boolean disableConsole) { - - try { - String devonIdeHome = System.getenv("DEVON_IDE_HOME"); - File home = null; - if (devonIdeHome != null) { - home = new File(devonIdeHome); - } - if ((home == null) || !home.isDirectory()) { - home = new File("."); - } - String logFolderName = "log"; - if (!new File(home, "scripts").exists()) { - logFolderName = "target"; - } - File logFolder = new File(home, logFolderName); - logFolder.mkdirs(); - File logFile = new File(logFolder, filename + ".log"); - if (logFile.exists()) { - logFile.delete(); - } - FileHandler logFileHandler = new FileHandler(logFile.getPath()); - logFileHandler.setLevel(Level.FINEST); - LogFormatter formatter = new LogFormatter(); - logFileHandler.setFormatter(formatter); - if (disableConsole) { - LOGGER.setUseParentHandlers(false); - } - LOGGER.addHandler(logFileHandler); - LOGGER.setLevel(Level.FINEST); - Logger globalLogger = Logger.getLogger(""); - globalLogger.setLevel(Level.FINEST); - for (Handler handler : globalLogger.getHandlers()) { - handler.setFormatter(formatter); - } - } catch (Exception e) { - System.err.println("Could not open log file"); - e.printStackTrace(); - } - } - - /** - * @param message the trace message to log. - */ - public static void trace(String message) { - - Log.LOGGER.finest(message); - } - - /** - * @param message the debug message to log. - */ - public static void debug(String message) { - - Log.LOGGER.fine(message); - } - - /** - * @param message the info message to log. - */ - public static void info(String message) { - - Log.LOGGER.info(message); - } - - /** - * @param message the warning message to log. - */ - public static void warn(String message) { - - Log.LOGGER.warning(message); - } - - /** - * @param message the error message to log. - */ - public static void err(String message) { - - Log.LOGGER.severe(message); - } - - /** - * @param message the error message to log. - * @param e the {@link Throwable} to log. - */ - public static void err(String message, Throwable e) { - - Log.LOGGER.log(Level.SEVERE, message, e); - } - - /** - * Custom log {@link Formatter} to overrule ugly JUL defaults. - */ - public static class LogFormatter extends Formatter { - - @Override - public String format(LogRecord record) { - - return String.format("%1$s [%2$-7s] %3$s\n", - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").format(new Date(record.getMillis())), - record.getLevel().getName(), formatMessage(record)); - } - } -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/logging/Output.java b/cli/src/main/java/com/devonfw/tools/ide/logging/Output.java deleted file mode 100644 index 2285252fd..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/logging/Output.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.devonfw.tools.ide.logging; - -/** - * Simple wrapper to print out to console and logger. - */ -public class Output { - - private static final String BANNER = "***********************************************************"; - - private static final Output INSTANCE = new Output(); - - /** - * Print {@link #info(String) info} surrounded with a banner. - * - * @param message the message text/template. - * @param args the optional arguments to fill into the message. - */ - public void banner(String message, Object... args) { - - info(BANNER); - info(message, args); - info(BANNER); - } - - /** - * Print the message to console and log. - * - * @param message the message text/template. - * @param args the optional arguments to fill into the message. - */ - public void info(String message, Object... args) { - - info(String.format(message, args)); - } - - /** - * Print the message to console and log. - * - * @param message the plain message. - */ - public void info(String message) { - - System.out.println(message); - Log.info(message); - } - - /** - * Log debug message. - * - * @param message the plain message. - */ - public void debug(String message) { - - Log.debug(message); - } - - /** - * Print the message as warning to console and log. - * - * @param message the message text/template. - * @param args the optional arguments to fill into the message. - */ - public void warn(String message, Object... args) { - - warn(String.format(message, args)); - } - - /** - * Print the message as warning to console and log. - * - * @param message the plain message. - */ - public void warn(String message) { - - System.out.println("WARNING: " + message); - Log.warn(message); - } - - /** - * Print the error to console and log. - * - * @param e the {@link Exception}. - */ - public void err(Exception e) { - - err(e.toString(), e); - } - - /** - * Print the message as error to console and log. - * - * @param message the message text/template. - * @param args the optional arguments to fill into the message. - */ - public void err(String message, Object... args) { - - err(String.format(message, args), (Exception) null); - } - - /** - * Print the message as error to console and log. - * - * @param message the plain message. - * @param e the {@link Exception} or {@code null} for none. - */ - public void err(String message, Exception e) { - - System.out.println("ERROR: " + message); - Log.err(message, e); - } - - /** - * @return get the instance of {@link Output}. - */ - public static Output get() { - - return INSTANCE; - } - -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/AbstractWorkspaceMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/AbstractWorkspaceMerger.java new file mode 100644 index 000000000..ef04ebbc0 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/AbstractWorkspaceMerger.java @@ -0,0 +1,46 @@ +package com.devonfw.tools.ide.merge; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.devonfw.tools.ide.context.IdeContext; + +/** + * {@link WorkspaceMerger} responsible for a single type of {@link Path}. + * + * @since 3.0.0 + */ +public abstract class AbstractWorkspaceMerger implements WorkspaceMerger { + + /** The {@link IdeContext} for logging. */ + protected final IdeContext context; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public AbstractWorkspaceMerger(IdeContext context) { + + super(); + this.context = context; + } + + /** + * @param file the {@link Path} for which the {@link Path#getParent() parent directory} needs to exist and will be + * created if absent by this method. + */ + protected static void ensureParentDirectoryExists(Path file) { + + Path parentDir = file.getParent(); + if (!Files.exists(parentDir)) { + try { + Files.createDirectories(parentDir); + } catch (IOException e) { + throw new IllegalStateException("Could not create required directories for file: " + file, e); + } + } + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/DirectoryMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/DirectoryMerger.java new file mode 100644 index 000000000..612117c43 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/DirectoryMerger.java @@ -0,0 +1,137 @@ +package com.devonfw.tools.ide.merge; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.jline.utils.Log; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.util.FilenameUtil; + +/** + * Implementation of {@link WorkspaceMerger} that does the whole thing: + *
    + *
  • It will recursively traverse directories.
  • + *
  • For each setup or update file if will delegate to the according {@link FileMerger} based on the file + * extension.
  • + *
+ */ +public class DirectoryMerger extends AbstractWorkspaceMerger { + + private final Map extension2mergerMap; + + private final FallbackMerger fallbackMerger; + + /** + * The constructor. + * + * @param context the {@link #context}. + */ + public DirectoryMerger(IdeContext context) { + + super(context); + this.extension2mergerMap = new HashMap<>(); + PropertiesMerger propertiesMerger = new PropertiesMerger(context); + this.extension2mergerMap.put("properties", propertiesMerger); + this.extension2mergerMap.put("prefs", propertiesMerger); // Eclipse specific + XmlMerger xmlMerger = new XmlMerger(context); + this.extension2mergerMap.put("xml", xmlMerger); + this.extension2mergerMap.put("xmi", xmlMerger); + this.extension2mergerMap.put("launch", xmlMerger); // Eclipse specific + JsonMerger jsonMerger = new JsonMerger(context); + this.extension2mergerMap.put("json", jsonMerger); + this.fallbackMerger = new FallbackMerger(context); + } + + @Override + public void merge(Path setup, Path update, EnvironmentVariables variables, Path workspace) { + + Set children = null; + children = addChildren(setup, children); + children = addChildren(update, children); + if (children == null) { + // file merge + FileMerger merger = getMerger(workspace); + merger.merge(setup, update, variables, workspace); + } else { + // directory scan + for (String filename : children) { + merge(setup.resolve(filename), update.resolve(filename), variables, workspace.resolve(filename)); + } + } + } + + private FileMerger getMerger(Path file) { + + String filename = file.getFileName().toString(); + String extension = FilenameUtil.getExtension(filename); + if (extension == null) { + this.context.debug("No extension for {}", file); + } else { + this.context.trace("Extension is {}", extension); + FileMerger merger = this.extension2mergerMap.get(extension); + if (merger != null) { + return merger; + } + } + return this.fallbackMerger; + } + + @Override + public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update) { + + if (Files.isDirectory(update)) { + if (!Files.isDirectory(workspace)) { + Log.warn("Workspace is missing directory: {}", workspace); + return; + } + Log.trace("Traversing directory: {}", update); + try { + Iterator iterator = Files.list(update).iterator(); + while (iterator.hasNext()) { + Path updateChild = iterator.next(); + Path fileName = updateChild.getFileName(); + inverseMerge(workspace.resolve(fileName), variables, addNewProperties, update.resolve(fileName)); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to list children of folder " + update, e); + } + + } else if (Files.exists(workspace)) { + Log.debug("Start merging of changes from workspace back to file: {}", update); + FileMerger merger = getMerger(workspace); + Log.trace("Using merger {}", merger.getClass().getSimpleName()); + merger.inverseMerge(workspace, variables, addNewProperties, update); + } else { + Log.warn("No such file or directory: {}", update); + } + } + + private Set addChildren(Path folder, Set children) { + + if (!Files.isDirectory(folder)) { + return children; + } + try { + Iterator iterator = Files.list(folder).iterator(); + while (iterator.hasNext()) { + Path child = iterator.next(); + if (children == null) { + children = new HashSet<>(); + } + children.add(child.getFileName().toString()); + } + return children; + } catch (IOException e) { + throw new IllegalStateException("Failed to list children of folder " + folder, e); + } + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/FallbackMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/FallbackMerger.java new file mode 100644 index 000000000..a930d0355 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/FallbackMerger.java @@ -0,0 +1,55 @@ +package com.devonfw.tools.ide.merge; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; + +/** + * Implementation of {@link FileMerger} to use as fallback. It can not actually merge but will simply overwrite the + * files. + * + * @since 3.0.0 + */ +public class FallbackMerger extends FileMerger { + + /** + * The constructor. + * + * @param context the {@link #context}. + */ + public FallbackMerger(IdeContext context) { + + super(context); + } + + @Override + public void merge(Path setup, Path update, EnvironmentVariables variables, Path workspace) { + + if (Files.exists(update)) { + copy(update, workspace); + } else if (Files.exists(setup) && !Files.exists(workspace)) { + copy(setup, workspace); + } + } + + @Override + public void inverseMerge(Path workspaceFile, EnvironmentVariables resolver, boolean addNewProperties, + Path updateFile) { + + // nothing by default, we could copy the workspace file back to the update file if it exists... + } + + private void copy(Path sourceFile, Path targetFile) { + + ensureParentDirectoryExists(targetFile); + try { + Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + throw new IllegalStateException("Failed to copy file " + sourceFile + " to " + targetFile, e); + } + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/FileMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/FileMerger.java new file mode 100644 index 000000000..4ea494b4c --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/FileMerger.java @@ -0,0 +1,20 @@ +package com.devonfw.tools.ide.merge; + +import com.devonfw.tools.ide.context.IdeContext; + +/** + * {@link WorkspaceMerger} responsible for a single type of file. + */ +public abstract class FileMerger extends AbstractWorkspaceMerger { + + /** + * The constructor. + * + * @param context the {@link #context}. + */ + public FileMerger(IdeContext context) { + + super(context); + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/JsonMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java similarity index 64% rename from cli/src/main/java/com/devonfw/tools/ide/configurator/merge/JsonMerger.java rename to cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java index f2c06162e..04e011a9c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/JsonMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java @@ -1,12 +1,9 @@ -package com.devonfw.tools.ide.configurator.merge; +package com.devonfw.tools.ide.merge; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.Reader; -import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -25,63 +22,69 @@ import javax.json.JsonWriterFactory; import javax.json.stream.JsonGenerator; -import com.devonfw.tools.ide.configurator.resolve.VariableResolver; -import com.devonfw.tools.ide.logging.Log; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; /** - * Implementation of {@link FileTypeMerger} for JSON. - * - * @since 3.0.0 + * Implementation of {@link FileMerger} for JSON. */ -public class JsonMerger extends FileTypeMerger { +public class JsonMerger extends FileMerger { + + /** + * The constructor. + * + * @param context the {@link #context}. + */ + public JsonMerger(IdeContext context) { + + super(context); + } @Override - public void merge(File setupFile, File updateFile, VariableResolver resolver, File workspaceFile) { + public void merge(Path setup, Path update, EnvironmentVariables variables, Path workspace) { JsonStructure json = null; - boolean updateFileExists = updateFile.exists(); - if (workspaceFile.exists()) { + boolean updateFileExists = Files.exists(update); + if (Files.exists(workspace)) { if (!updateFileExists) { return; // nothing to do ... } - json = load(workspaceFile); - } else if (setupFile.exists()) { - json = load(setupFile); + json = load(workspace); + } else if (Files.exists(setup)) { + json = load(setup); } JsonStructure mergeJson = null; if (updateFileExists) { if (json == null) { - json = load(updateFile); + json = load(update); } else { - mergeJson = load(updateFile); + mergeJson = load(update); } } Status status = new Status(); - JsonStructure result = (JsonStructure) mergeAndResolve(json, mergeJson, resolver, status); + JsonStructure result = (JsonStructure) mergeAndResolve(json, mergeJson, variables, status, workspace.getFileName()); if (status.updated) { - save(result, workspaceFile); - Log.debug("Saving created/updated file: " + workspaceFile.getAbsolutePath()); + save(result, workspace); + this.context.debug("Saved created/updated file {}", workspace); } else { - Log.trace("No changes for file: " + workspaceFile.getAbsolutePath()); + this.context.trace("No changes for file {}", workspace); } } - private static JsonStructure load(File file) { - - try (FileInputStream in = new FileInputStream(file); - Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + private static JsonStructure load(Path file) { - JsonReader jsonReader = Json.createReader(new BufferedReader(reader)); + try (Reader reader = Files.newBufferedReader(file)) { + JsonReader jsonReader = Json.createReader(reader); return jsonReader.read(); } catch (Exception e) { - throw new IllegalStateException("Failed to read JSON from " + file.getPath(), e); + throw new IllegalStateException("Failed to read JSON from " + file, e); } } - private static void save(JsonStructure json, File file) { + private static void save(JsonStructure json, Path file) { - ensureParentDirecotryExists(file); - try (FileOutputStream out = new FileOutputStream(file)) { + ensureParentDirectoryExists(file); + try (OutputStream out = Files.newOutputStream(file)) { Map config = new HashMap<>(); config.put(JsonGenerator.PRETTY_PRINTING, Boolean.TRUE); @@ -94,35 +97,37 @@ private static void save(JsonStructure json, File file) { jsonWriter.write(json); jsonWriter.close(); } catch (Exception e) { - throw new IllegalStateException("Failed to save JSON to " + file.getPath(), e); + throw new IllegalStateException("Failed to save JSON to " + file, e); } } @Override - public void inverseMerge(File workspaceFile, VariableResolver resolver, boolean addNewProperties, File updateFile) { + public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path updateFile) { - if (!workspaceFile.exists() || !updateFile.exists()) { + if (!Files.exists(workspace) || !Files.exists(updateFile)) { return; } JsonStructure updateDocument = load(updateFile); - JsonStructure workspaceDocument = load(workspaceFile); + JsonStructure workspaceDocument = load(workspace); Status status = new Status(addNewProperties); - JsonStructure result = (JsonStructure) mergeAndResolve(workspaceDocument, updateDocument, resolver, status); + JsonStructure result = (JsonStructure) mergeAndResolve(workspaceDocument, updateDocument, variables, status, + workspace.getFileName()); if (status.updated) { save(result, updateFile); - Log.debug("Saved changes from " + workspaceFile.getName() + " to " + updateFile.getAbsolutePath()); + this.context.debug("Saved changes from {} to {}", workspace.getFileName(), updateFile); } else { - Log.trace("No changes for " + updateFile.getAbsolutePath()); + this.context.trace("No changes for {}", updateFile); } } - private JsonValue mergeAndResolve(JsonValue json, JsonValue mergeJson, VariableResolver resolver, Status status) { + private JsonValue mergeAndResolve(JsonValue json, JsonValue mergeJson, EnvironmentVariables variables, Status status, + Object src) { if (json == null) { if (mergeJson == null) { return null; } else { - return mergeAndResolve(mergeJson, null, resolver, status); + return mergeAndResolve(mergeJson, null, variables, status, src); } } else { if (mergeJson == null) { @@ -130,25 +135,25 @@ private JsonValue mergeAndResolve(JsonValue json, JsonValue mergeJson, VariableR } switch (json.getValueType()) { case OBJECT: - return mergeAndResolveObject((JsonObject) json, (JsonObject) mergeJson, resolver, status); + return mergeAndResolveObject((JsonObject) json, (JsonObject) mergeJson, variables, status, src); case ARRAY: - return mergeAndResolveArray((JsonArray) json, (JsonArray) mergeJson, resolver, status); + return mergeAndResolveArray((JsonArray) json, (JsonArray) mergeJson, variables, status, src); case STRING: - return mergeAndResolveString((JsonString) json, (JsonString) mergeJson, resolver, status); + return mergeAndResolveString((JsonString) json, (JsonString) mergeJson, variables, status, src); case NUMBER: case FALSE: case TRUE: case NULL: - return mergeAndResolveNativeType(json, mergeJson, resolver, status); + return mergeAndResolveNativeType(json, mergeJson, variables, status); default: - Log.err("Undefined JSON type: " + json.getClass()); + this.context.error("Undefined JSON type {}", json.getClass()); return null; } } } - private JsonObject mergeAndResolveObject(JsonObject json, JsonObject mergeJson, VariableResolver resolver, - Status status) { + private JsonObject mergeAndResolveObject(JsonObject json, JsonObject mergeJson, EnvironmentVariables variables, + Status status, Object src) { // json = workspace/setup // mergeJson = update @@ -159,7 +164,7 @@ private JsonObject mergeAndResolveObject(JsonObject json, JsonObject mergeJson, for (String key : mergeKeySet) { JsonValue mergeValue = mergeJson.get(key); JsonValue value = json.get(key); - value = mergeAndResolve(value, mergeValue, resolver, status); + value = mergeAndResolve(value, mergeValue, variables, status, src); builder.add(key, value); } } @@ -167,7 +172,7 @@ private JsonObject mergeAndResolveObject(JsonObject json, JsonObject mergeJson, for (String key : json.keySet()) { if (!mergeKeySet.contains(key)) { JsonValue value = json.get(key); - value = mergeAndResolve(value, null, resolver, status); + value = mergeAndResolve(value, null, variables, status, src); builder.add(key, value); if (status.inverse) { // added new property on inverse merge... @@ -179,8 +184,8 @@ private JsonObject mergeAndResolveObject(JsonObject json, JsonObject mergeJson, return builder.build(); } - private JsonArray mergeAndResolveArray(JsonArray json, JsonArray mergeJson, VariableResolver resolver, - Status status) { + private JsonArray mergeAndResolveArray(JsonArray json, JsonArray mergeJson, EnvironmentVariables variables, + Status status, Object src) { JsonArrayBuilder builder = Json.createArrayBuilder(); // KISS: Merging JSON arrays could be very complex. We simply let mergeJson override json... @@ -189,14 +194,14 @@ private JsonArray mergeAndResolveArray(JsonArray json, JsonArray mergeJson, Vari source = mergeJson; } for (JsonValue value : source) { - JsonValue resolvedValue = mergeAndResolve(value, null, resolver, status); + JsonValue resolvedValue = mergeAndResolve(value, null, variables, status, src); builder.add(resolvedValue); } return builder.build(); } - private JsonString mergeAndResolveString(JsonString json, JsonString mergeJson, VariableResolver resolver, - Status status) { + private JsonString mergeAndResolveString(JsonString json, JsonString mergeJson, EnvironmentVariables variables, + Status status, Object src) { JsonString jsonString = json; if (mergeJson != null) { @@ -205,9 +210,9 @@ private JsonString mergeAndResolveString(JsonString json, JsonString mergeJson, String string = jsonString.getString(); String resolvedString; if (status.inverse) { - resolvedString = resolver.inverseResolve(string); + resolvedString = variables.inverseResolve(string, src); } else { - resolvedString = resolver.resolve(string); + resolvedString = variables.resolve(string, src); } if (!resolvedString.equals(string)) { status.updated = true; @@ -215,7 +220,7 @@ private JsonString mergeAndResolveString(JsonString json, JsonString mergeJson, return Json.createValue(resolvedString); } - private JsonValue mergeAndResolveNativeType(JsonValue json, JsonValue mergeJson, VariableResolver resolver, + private JsonValue mergeAndResolveNativeType(JsonValue json, JsonValue mergeJson, EnvironmentVariables variables, Status status) { if (mergeJson == null) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/PropertiesMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/PropertiesMerger.java new file mode 100644 index 000000000..553b1869f --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/PropertiesMerger.java @@ -0,0 +1,160 @@ +package com.devonfw.tools.ide.merge; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; +import java.util.Set; + +import org.jline.utils.Log; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.environment.SortedProperties; + +/** + * Implementation of {@link FileMerger} for {@link Properties} files. + */ +public class PropertiesMerger extends FileMerger { + + /** + * The constructor. + * + * @param context the {@link #context}. + */ + public PropertiesMerger(IdeContext context) { + + super(context); + } + + @Override + public void merge(Path setup, Path update, EnvironmentVariables resolver, Path workspace) { + + SortedProperties properties = new SortedProperties(); + boolean updateFileExists = Files.exists(update); + if (Files.exists(workspace)) { + if (!updateFileExists) { + Log.trace("Nothing to do as update file does not exist: {}", update); + return; // nothing to do ... + } + load(properties, workspace); + } else if (Files.exists(setup)) { + load(properties, setup); + } + if (updateFileExists) { + load(properties, update); + } + resolve(properties, resolver, workspace.getFileName()); + save(properties, workspace); + Log.trace("Saved merged properties to: {}", workspace); + } + + /** + * @param file the {@link Path} to load. + * @return the loaded {@link Properties}. + */ + public static Properties load(Path file) { + + Properties properties = new Properties(); + load(properties, file); + return properties; + } + + /** + * @param file the {@link Path} to load. + * @return the loaded {@link Properties}. + */ + public static Properties loadIfExists(Path file) { + + Properties properties = new Properties(); + if (file != null) { + if (Files.exists(file)) { + load(properties, file); + } else { + Log.trace("Properties file does not exist: {}", file); + } + } + return properties; + } + + /** + * @param properties the existing {@link Properties} instance. + * @param file the properties {@link Path} to load. + */ + public static void load(Properties properties, Path file) { + + Log.trace("Loading properties file: {}", file); + try (Reader reader = Files.newBufferedReader(file)) { + properties.load(reader); + } catch (IOException e) { + throw new IllegalStateException("Could not load properties from file: " + file, e); + } + } + + private void resolve(Properties properties, EnvironmentVariables variables, Object src) { + + Set keys = properties.keySet(); + for (Object key : keys) { + String value = properties.getProperty(key.toString()); + properties.setProperty(key.toString(), variables.resolve(value, src)); + } + } + + /** + * @param properties the {@link Properties} to save. + * @param file the {@link Path} to save to. + */ + public static void save(Properties properties, Path file) { + + Log.trace("Saving properties file: {}", file); + ensureParentDirectoryExists(file); + try (Writer writer = Files.newBufferedWriter(file)) { + properties.store(writer, null); + } catch (IOException e) { + throw new IllegalStateException("Could not write properties to file: " + file, e); + } + } + + @Override + public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update) { + + if (!Files.exists(workspace)) { + Log.trace("Workspace file does not exist: {}", workspace); + return; + } + if (!Files.exists(update)) { + Log.trace("Update file does not exist: {}", update); + return; + } + Object src = workspace.getFileName(); + Properties updateProperties = load(update); + Properties workspaceProperties = load(workspace); + SortedProperties mergedProperties = new SortedProperties(); + mergedProperties.putAll(updateProperties); + boolean updated = false; + for (Object key : workspaceProperties.keySet()) { + Object workspaceValue = workspaceProperties.get(key); + Object updateValue = updateProperties.get(key); + if ((updateValue != null) || addNewProperties) { + String updateValueResolved = null; + if (updateValue != null) { + updateValueResolved = variables.resolve(updateValue.toString(), src); + } + if (!workspaceValue.equals(updateValueResolved)) { + String workspaceValueInverseResolved = variables.inverseResolve(workspaceValue.toString(), src); + mergedProperties.put(key, workspaceValueInverseResolved); + updated = true; + } + } + } + if (updated) { + save(mergedProperties, update); + Log.debug("Saved changes from: {} to: {}", workspace.getFileName(), update); + } else { + Log.trace("No changes for: {}", update); + } + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/WorkspaceMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/WorkspaceMerger.java new file mode 100644 index 000000000..3efce43ac --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/WorkspaceMerger.java @@ -0,0 +1,32 @@ +package com.devonfw.tools.ide.merge; + +import java.nio.file.Path; + +import com.devonfw.tools.ide.environment.EnvironmentVariables; + +/** + * Interface for a merger responsible for merging {@link com.devonfw.tools.ide.tool.ide.IdeToolCommandlet IDE} + * configuration files into the workspace. + */ +public interface WorkspaceMerger { + + /** + * @param setup the setup {@link Path} for creation. + * @param update the update {@link Path} for creation and update. + * @param variables the {@link EnvironmentVariables} to {@link EnvironmentVariables#resolve(String, Object) resolve + * variables}. + * @param workspace the workspace {@link Path} to create or update. + */ + void merge(Path setup, Path update, EnvironmentVariables variables, Path workspace); + + /** + * @param workspace the workspace {@link Path} where to get the changes from. + * @param variables the {@link EnvironmentVariables} to {@link EnvironmentVariables#inverseResolve(String, Object) + * inverse resolve variables}. + * @param addNewProperties - {@code true} to also add new properties to the {@code updateFile}, {@code false} + * otherwise (to only update existing properties). + * @param update the update {@link Path} + */ + void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update); + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/XmlMerger.java b/cli/src/main/java/com/devonfw/tools/ide/merge/XmlMerger.java similarity index 65% rename from cli/src/main/java/com/devonfw/tools/ide/configurator/merge/XmlMerger.java rename to cli/src/main/java/com/devonfw/tools/ide/merge/XmlMerger.java index c72bc482f..4be18367c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/configurator/merge/XmlMerger.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/XmlMerger.java @@ -1,6 +1,8 @@ -package com.devonfw.tools.ide.configurator.merge; +package com.devonfw.tools.ide.merge; -import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -17,15 +19,13 @@ import org.w3c.dom.NodeList; import org.w3c.dom.Text; -import com.devonfw.tools.ide.configurator.resolve.VariableResolver; -import com.devonfw.tools.ide.logging.Log; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; /** - * Implementation of {@link FileTypeMerger} for XML. - * - * @since 3.0.0 + * Implementation of {@link FileMerger} for XML files. */ -public class XmlMerger extends FileTypeMerger { +public class XmlMerger extends FileMerger { private static final DocumentBuilder DOCUMENT_BUILDER; @@ -44,35 +44,37 @@ public class XmlMerger extends FileTypeMerger { /** * The constructor. + * + * @param context the {@link #context}. */ - public XmlMerger() { + public XmlMerger(IdeContext context) { - super(); + super(context); } @Override - public void merge(File setupFile, File updateFile, VariableResolver resolver, File workspaceFile) { + public void merge(Path setup, Path update, EnvironmentVariables resolver, Path workspace) { Document document = null; - boolean updateFileExists = updateFile.exists(); - if (workspaceFile.exists()) { + boolean updateFileExists = Files.exists(update); + if (Files.exists(workspace)) { if (!updateFileExists) { return; // nothing to do ... } - document = load(workspaceFile); - } else if (setupFile.exists()) { - document = load(setupFile); + document = load(workspace); + } else if (Files.exists(setup)) { + document = load(setup); } if (updateFileExists) { if (document == null) { - document = load(updateFile); + document = load(update); } else { - Document updateDocument = load(updateFile); + Document updateDocument = load(update); merge(updateDocument, document, true, true); } } - resolve(document, resolver, false); - save(document, workspaceFile); + resolve(document, resolver, false, workspace.getFileName()); + save(document, workspace); } private void merge(Document sourceDocument, Document targetDocument, boolean override, boolean add) { @@ -120,27 +122,27 @@ private void merge(NamedNodeMap sourceAttributes, Element targetElement, boolean } @Override - public void inverseMerge(File workspaceFile, VariableResolver resolver, boolean addNewProperties, File updateFile) { + public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update) { - if (!workspaceFile.exists() || !updateFile.exists()) { + if (!Files.exists(workspace) || !Files.exists(update)) { return; } - Document updateDocument = load(updateFile); - Document workspaceDocument = load(workspaceFile); + Document updateDocument = load(update); + Document workspaceDocument = load(workspace); merge(workspaceDocument, updateDocument, true, addNewProperties); - resolve(updateDocument, resolver, true); - save(updateDocument, updateFile); - Log.debug("Saved changes in " + workspaceFile.getName() + " to: " + updateFile.getAbsolutePath()); + resolve(updateDocument, variables, true, workspace.getFileName()); + save(updateDocument, update); + this.context.debug("Saved changes in {} to {}", workspace.getFileName(), update); } /** - * @param file the {@link File} to load. + * @param file the {@link Path} to load. * @return the loaded XML {@link Document}. */ - public static Document load(File file) { + public static Document load(Path file) { - try { - return DOCUMENT_BUILDER.parse(file); + try (InputStream in = Files.newInputStream(file)) { + return DOCUMENT_BUILDER.parse(in); } catch (Exception e) { throw new IllegalStateException("Failed to load XML from: " + file, e); } @@ -148,15 +150,15 @@ public static Document load(File file) { /** * @param document the XML {@link Document} to save. - * @param file the {@link File} to save to. + * @param file the {@link Path} to save to. */ - public static void save(Document document, File file) { + public static void save(Document document, Path file) { - ensureParentDirecotryExists(file); + ensureParentDirectoryExists(file); try { Transformer transformer = TRANSFORMER_FACTORY.newTransformer(); DOMSource source = new DOMSource(document); - StreamResult result = new StreamResult(file); + StreamResult result = new StreamResult(file.toFile()); transformer.transform(source, result); } catch (Exception e) { throw new IllegalStateException("Failed to save XML to file: " + file, e); @@ -164,18 +166,18 @@ public static void save(Document document, File file) { } - private void resolve(Document document, VariableResolver resolver, boolean inverse) { + private void resolve(Document document, EnvironmentVariables resolver, boolean inverse, Object src) { NodeList nodeList = document.getElementsByTagName("*"); for (int i = 0; i < nodeList.getLength(); i++) { Element element = (Element) nodeList.item(i); - resolve(element, resolver, inverse); + resolve(element, resolver, inverse, src); } } - private void resolve(Element element, VariableResolver resolver, boolean inverse) { + private void resolve(Element element, EnvironmentVariables variables, boolean inverse, Object src) { - resolve(element.getAttributes(), resolver, inverse); + resolve(element.getAttributes(), variables, inverse, src); NodeList nodeList = element.getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { @@ -185,25 +187,25 @@ private void resolve(Element element, VariableResolver resolver, boolean inverse String value = text.getNodeValue(); String resolvedValue; if (inverse) { - resolvedValue = resolver.inverseResolve(value); + resolvedValue = variables.inverseResolve(value, src); } else { - resolvedValue = resolver.resolve(value); + resolvedValue = variables.resolve(value, src); } text.setNodeValue(resolvedValue); } } } - private void resolve(NamedNodeMap attributes, VariableResolver resolver, boolean inverse) { + private void resolve(NamedNodeMap attributes, EnvironmentVariables variables, boolean inverse, Object src) { for (int i = 0; i < attributes.getLength(); i++) { Attr attribute = (Attr) attributes.item(i); String value = attribute.getValue(); String resolvedValue; if (inverse) { - resolvedValue = resolver.inverseResolve(value); + resolvedValue = variables.inverseResolve(value, src); } else { - resolvedValue = resolver.resolve(value); + resolvedValue = variables.resolve(value, src); } attribute.setValue(resolvedValue); } 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 c25a7d5bb..f5ab8b3be 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 @@ -65,8 +65,7 @@ public String getName() { } /** - * - * @return the name of the binary + * @return the name of the binary executable for this tool. */ protected String getBinaryName() { @@ -283,6 +282,9 @@ protected boolean isExtract() { return true; } + /** + * @return the {@link MacOsHelper} instance. + */ protected MacOsHelper getMacOsHelper() { if (this.macOsHelper == null) { @@ -300,6 +302,7 @@ public VersionIdentifier getInstalledVersion() { } /** + * @param toolPath the installation {@link Path} where to find the version file. * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed. */ protected VersionIdentifier getInstalledVersion(Path toolPath) { 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 1d0adf78c..38116d067 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 @@ -1,10 +1,16 @@ package com.devonfw.tools.ide.tool.eclipse; +import java.io.File; +import java.io.RandomAccessFile; +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; +import com.devonfw.tools.ide.cli.CliArgument; +import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.process.ProcessContext; @@ -15,7 +21,7 @@ import com.devonfw.tools.ide.tool.java.Java; /** - * {@link IdeToolCommandlet} for Eclipse IDE. + * {@link IdeToolCommandlet} for Eclipse. */ public class Eclipse extends IdeToolCommandlet { @@ -30,10 +36,11 @@ public Eclipse(IdeContext context) { } @Override - public void run() { + protected void runIde(String... args) { - install(); - runEclipse(false, this.arguments.asArray()); + install(true); + runEclipse(false, + CliArgument.prepend(args, "gui", "-showlocation", this.context.getIdeHome().getFileName().toString())); } @Override @@ -104,4 +111,33 @@ private void log(IdeLogLevel level, List lines) { } } + @Override + protected void configureWorkspace() { + + Path lockfile = this.context.getWorkspacePath().resolve(".metadata/.lock"); + if (isLocked(lockfile)) { + throw new CliException("Your workspace is locked at " + lockfile); + } + super.configureWorkspace(); + } + + /** + * @param lockfile the {@link File} pointing to the lockfile to check. + * @return {@code true} if the given {@link File} is locked, {@code false} otherwise. + */ + private static boolean isLocked(Path lockfile) { + + if (Files.isRegularFile(lockfile)) { + try (RandomAccessFile raFile = new RandomAccessFile(lockfile.toFile(), "rw")) { + FileLock fileLock = raFile.getChannel().tryLock(0, 1, false); + // success, file was not locked so we immediately unlock again... + fileLock.release(); + return false; + } catch (Exception e) { + return true; + } + } + return false; + } + } 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 c517a51f4..44c2cdd3f 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 @@ -16,9 +16,13 @@ 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.eclipse.Eclipse; +import com.devonfw.tools.ide.tool.intellij.Intellij; +import com.devonfw.tools.ide.tool.vscode.Vscode; /** - * {@link ToolCommandlet} for an IDE (integrated development environment). + * {@link ToolCommandlet} for an IDE (integrated development environment) such as {@link Eclipse}, {@link Vscode}, or + * {@link Intellij}. */ public abstract class IdeToolCommandlet extends LocalToolCommandlet { @@ -178,4 +182,45 @@ public void uninstallPlugin(PluginDescriptor plugin) { fileAccess.delete(match); } + @Override + public void run() { + + configureWorkspace(); + runIde(this.arguments.asArray()); + } + + /** + * Run the actual IDE. + * + * @param args the additional arguments to pass to the IDE. + */ + protected void runIde(String... args) { + + runTool(null, args); + } + + /** + * Configure the workspace for this IDE using the templates from the settings. + */ + protected void configureWorkspace() { + + Path settingsWorkspaceFolder = this.context.getSettingsPath().resolve(this.tool) + .resolve(IdeContext.FOLDER_WORKSPACE); + FileAccess fileAccess = this.context.getFileAccess(); + if (!fileAccess.isExpectedFolder(settingsWorkspaceFolder)) { + return; + } + Path setupFolder = settingsWorkspaceFolder.resolve(IdeContext.FOLDER_SETUP); + Path updateFolder = settingsWorkspaceFolder.resolve(IdeContext.FOLDER_UPDATE); + if (!fileAccess.isExpectedFolder(setupFolder) && !fileAccess.isExpectedFolder(updateFolder)) { + return; + } + Path ideWorkspacePath = this.context.getWorkspacePath(); + if (!fileAccess.isExpectedFolder(ideWorkspacePath)) { + return; // should actually never happen... + } + this.context.step("Configuring workspace {} for IDE {}", ideWorkspacePath.getFileName(), this.tool); + this.context.getWorkspaceMerger().merge(setupFolder, updateFolder, this.context.getVariables(), ideWorkspacePath); + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java b/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java new file mode 100644 index 000000000..cbab7deef --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java @@ -0,0 +1,39 @@ +package com.devonfw.tools.ide.tool.intellij; + +import java.util.Set; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet; +import com.devonfw.tools.ide.tool.ide.PluginDescriptor; +import com.devonfw.tools.ide.tool.java.Java; + +/** + * {@link IdeToolCommandlet} for IntelliJ. + */ +public class Intellij extends IdeToolCommandlet { + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public Intellij(IdeContext context) { + + super(context, "intellij", Set.of(TAG_JAVA, TAG_IDE)); + } + + @Override + public boolean install(boolean silent) { + + getCommandlet(Java.class).install(); + return super.install(silent); + } + + @Override + public void installPlugin(PluginDescriptor plugin) { + + // TODO Auto-generated method stub + + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java b/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java index 52715255d..45ffa5769 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java +++ b/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java @@ -50,7 +50,7 @@ public interface IdeVariables { VariableDefinitionString GRAALVM_EDITION = new VariableDefinitionString("GRAALVM_EDITION", null, c -> "community"); /** A {@link Collection} with all pre-defined {@link VariableDefinition}s. */ - Collection> VARIABLES = List.of(PATH, HOME, IDE_ROOT, IDE_HOME, WORKSPACE, WORKSPACE_PATH, + Collection> VARIABLES = List.of(PATH, HOME, WORKSPACE_PATH, IDE_HOME, IDE_ROOT, WORKSPACE, IDE_TOOLS, CREATE_START_SCRIPTS, IDE_MIN_VERSION, MVN_VERSION, DOCKER_EDITION, GRAALVM_EDITION); /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinition.java b/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinition.java index 6e45da6c0..162421f23 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinition.java +++ b/cli/src/main/java/com/devonfw/tools/ide/variable/VariableDefinition.java @@ -1,5 +1,7 @@ package com.devonfw.tools.ide.variable; +import java.nio.file.Path; + import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.VariableLine; @@ -71,6 +73,8 @@ default String toString(V value) { if (value == null) { return ""; + } else if (value instanceof Path) { + return value.toString().replace('\\', '/'); } return value.toString(); } 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 132b37a35..bc82d1f35 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 @@ -1,11 +1,12 @@ package com.devonfw.tools.ide.commandlet; -import com.devonfw.tools.ide.context.AbstractIdeContextTest; -import com.devonfw.tools.ide.context.IdeContext; +import java.io.IOException; +import java.nio.file.Path; + import org.junit.jupiter.api.Test; -import java.io.IOException; -import java.nio.file.Files; +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; /** * Integration test of {@link VersionSetCommandlet}. @@ -14,19 +15,35 @@ public class VersionSetCommandletTest extends AbstractIdeContextTest { /** * Test of {@link VersionSetCommandlet} run. + * * @throws IOException on error. */ @Test public void testVersionSetCommandletRun() throws IOException { - //arrange + + // arrange String path = "workspaces/foo-test/my-git-repo"; IdeContext context = newContext("basic", path, true); VersionSetCommandlet versionSet = context.getCommandletManager().getCommandlet(VersionSetCommandlet.class); versionSet.tool.setValueAsString("mvn"); versionSet.version.setValueAsString("3.1.0"); - //act + // act versionSet.run(); - //assert - assertThat(Files.readAllLines(context.getSettingsPath().resolve("ide.properties"))).contains("MVN_VERSION=3.1.0"); + // assert + Path settingsIdeProperties = context.getSettingsPath().resolve("ide.properties"); + assertThat(settingsIdeProperties).hasContent(""" + #******************************************************************************** + # This file contains project specific environment variables + #******************************************************************************** + + JAVA_VERSION=17* + MVN_VERSION=3.1.0 + ECLIPSE_VERSION=2023-03 + INTELLIJ_EDITION=ultimate + + IDE_TOOLS=mvn,eclipse + + BAR=bar-${SOME} + """); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/configurator/ConfiguratorTest.java b/cli/src/test/java/com/devonfw/tools/ide/configurator/ConfiguratorTest.java deleted file mode 100644 index deb8911c4..000000000 --- a/cli/src/test/java/com/devonfw/tools/ide/configurator/ConfiguratorTest.java +++ /dev/null @@ -1,200 +0,0 @@ -package com.devonfw.tools.ide.configurator; - -import java.io.File; -import java.io.IOException; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.time.Instant; -import java.util.Map.Entry; -import java.util.Properties; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -import com.devonfw.tools.ide.configurator.merge.PropertiesMerger; - -/** - * Test of {@link Configurator}. - */ -public class ConfiguratorTest extends Assertions { - - private static final String DEVON_IDE_HOME = new File("").getAbsolutePath().replace("\\", "/"); - - private static final Prop JAVA_VERSION = new Prop("java.version", "1.11"); - - private static final Prop JAVA_HOME = new Prop("java.home", DEVON_IDE_HOME + "/software/java"); - - private static final Prop THEME = new Prop("theme", "dark"); - - private static final Prop UI = new Prop("ui", "classic"); - - private static final Prop INDENTATION = new Prop("indentation", "2"); - - private static final Prop THEME_HACKED = new Prop("theme", "light"); - - private static final Prop UI_HACKED = new Prop("ui", "linux"); - - private static final Prop INDENTATION_HACKED = new Prop("indentation", "4"); - - private static final Prop JAVA_VERSION_HACKED = new Prop("java.version", "1.99"); - - private static final Prop EDITOR = new Prop("editor", "vi"); - - /** - * Test of {@link Configurator}. - * - * @throws Exception on error. - */ - @Test - public void testConfigurator() throws Exception { - - // given - Configurator configurator = new Configurator(); - String tmp = System.getProperty("java.io.tmpdir"); - File tmpDir = new File(tmp); - File workspaceDir = createUniqueFolder(tmpDir, ".test.workspace"); - String clientEnvHome = DEVON_IDE_HOME; - - // when - int exitCode = configurator.run("-t", "src/test/resources/templates", "-w", workspaceDir.getAbsolutePath(), "-u"); - - // then - assertThat(exitCode).isEqualTo(0); - File mainPrefsFile = new File(workspaceDir, "main.prefs"); - Properties mainPrefs = PropertiesMerger.load(mainPrefsFile); - assertThat(mainPrefs).containsOnly(JAVA_VERSION, JAVA_HOME, THEME, UI); - File jsonFolder = new File(workspaceDir, "json"); - assertThat(jsonFolder).isDirectory(); - assertThat(new File(jsonFolder, "settings.json")).hasContent("\n" // this newline is rather a bug of JSON-P impl - + "{\n" // - + " \"java.home\": \"" + DEVON_IDE_HOME + "/software/java\",\n" // - + " \"tslint.autoFixOnSave\": true,\n" // - + " \"object\": {\n" // - + " \"bar\": \"" + DEVON_IDE_HOME + "/bar\",\n" // - + " \"array\": [\n" // - + " \"a\",\n" // - + " \"b\",\n" // - + " \"" + DEVON_IDE_HOME + "\"\n" // - + " ],\n" // - + " \"foo\": \"" + DEVON_IDE_HOME + "/foo\"\n" // - + " }\n" // - + "}"); - assertThat(new File(jsonFolder, "update.json")).hasContent("\n" // this newline is rather a bug of JSON-P impl - + "{\n" // - + " \"key\": \"value\"\n" // - + "}"); - - File configFolder = new File(workspaceDir, "config"); - assertThat(configFolder).isDirectory(); - File indentFile = new File(configFolder, "indent.properties"); - Properties indent = PropertiesMerger.load(indentFile); - assertThat(indent).containsOnly(INDENTATION); - assertThat(new File(configFolder, "layout.xml")).hasContent("" // - + "" // here a newline would be reasonable... - + "\n" // - + " navigator\n" // - + " debugger\n" // - + " editor\n" // - + " console\n" // - + " " + clientEnvHome + "\n" // - + ""); - - // and after - EDITOR.apply(mainPrefs); - JAVA_VERSION_HACKED.apply(mainPrefs); - UI_HACKED.apply(mainPrefs); - THEME_HACKED.apply(mainPrefs); - INDENTATION_HACKED.apply(mainPrefs); - PropertiesMerger.save(mainPrefs, mainPrefsFile); - - // when - configurator = new Configurator(); - exitCode = configurator.run("-t", "src/test/resources/templates", "-w", workspaceDir.getAbsolutePath(), "-u"); - - // then - mainPrefs = PropertiesMerger.load(mainPrefsFile); - assertThat(mainPrefs).containsOnly(JAVA_VERSION, JAVA_HOME, THEME_HACKED, UI_HACKED, EDITOR, INDENTATION_HACKED); - - // finally cleanup - delete(workspaceDir.toPath()); - } - - private File createUniqueFolder(File tmpDir, String string) { - - File folder = new File(tmpDir, string); - File uniqueFolder = new File(folder, Instant.now().toString().replace(':', '_')); - boolean success = uniqueFolder.mkdirs(); - assert success : uniqueFolder.getAbsolutePath(); - return uniqueFolder; - } - - private static void delete(Path path) throws IOException { - - Files.walkFileTree(path, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { - - if (e != null) { - throw e; - } - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } - - private static class Prop implements Entry { - - private final String key; - - private String value; - - private Prop(String key, String value) { - - super(); - this.key = key; - this.value = value; - } - - @Override - public String getKey() { - - return this.key; - } - - @Override - public String getValue() { - - return this.value; - } - - @Override - public String setValue(String value) { - - throw new IllegalStateException(value); - } - - public void apply(Properties properties) { - - properties.setProperty(this.key, this.value); - } - - @Override - public String toString() { - - return this.key + "=" + this.value; - } - - } - -} 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 ba671cff3..287905742 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 @@ -64,9 +64,6 @@ protected static IdeTestContext newContext(String projectName, String projectPat Path sourceDir = PATH_PROJECTS.resolve(projectName); Path userDir = sourceDir; - if (projectPath != null) { - userDir = sourceDir.resolve(projectPath); - } CommandletManagerResetter.reset(); IdeTestContext context; if (copyForMutation) { @@ -77,10 +74,12 @@ protected static IdeTestContext newContext(String projectName, String projectPat fileAccess.copy(sourceDir, projectDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE); fileAccess.copy(PATH_PROJECTS.resolve(IdeContext.FOLDER_IDE), PATH_PROJECTS_COPY.resolve(IdeContext.FOLDER_IDE), FileCopyMode.COPY_TREE_OVERRIDE_TREE); - context = new IdeTestContext(projectDir.resolve(projectPath)); - } else { - context = new IdeTestContext(userDir); + userDir = projectDir; + } + if (projectPath != null) { + userDir = userDir.resolve(projectPath); } + context = new IdeTestContext(userDir); return context; } diff --git a/cli/src/test/java/com/devonfw/tools/ide/configurator/SortedPropertiesTest.java b/cli/src/test/java/com/devonfw/tools/ide/environment/SortedPropertiesTest.java similarity index 91% rename from cli/src/test/java/com/devonfw/tools/ide/configurator/SortedPropertiesTest.java rename to cli/src/test/java/com/devonfw/tools/ide/environment/SortedPropertiesTest.java index 553f98936..5a6fba48a 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/configurator/SortedPropertiesTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/environment/SortedPropertiesTest.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.configurator; +package com.devonfw.tools.ide.environment; import java.io.ByteArrayOutputStream; import java.io.OutputStreamWriter; @@ -6,6 +6,8 @@ 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/merge/DirectoryMergerTest.java b/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java new file mode 100644 index 000000000..55b0d37ba --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/merge/DirectoryMergerTest.java @@ -0,0 +1,166 @@ +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; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; + +import ch.qos.logback.classic.spi.Configurator; + +/** + * Test of {@link DirectoryMerger}. + */ +public class DirectoryMergerTest extends AbstractIdeContextTest { + + private static final String IDE_HOME = PATH_PROJECTS.resolve(PROJECT_BASIC).toAbsolutePath().toString().replace('\\', + '/'); + + private static final Prop JAVA_VERSION = new Prop("java.version", "1.11"); + + private static final Prop JAVA_HOME = new Prop("java.home", IDE_HOME + "/software/java"); + + private static final Prop THEME = new Prop("theme", "dark"); + + private static final Prop UI = new Prop("ui", "classic"); + + private static final Prop INDENTATION = new Prop("indentation", "2"); + + private static final Prop THEME_HACKED = new Prop("theme", "light"); + + private static final Prop UI_HACKED = new Prop("ui", "linux"); + + private static final Prop INDENTATION_HACKED = new Prop("indentation", "4"); + + private static final Prop JAVA_VERSION_HACKED = new Prop("java.version", "1.99"); + + private static final Prop EDITOR = new Prop("editor", "vi"); + + /** + * Test of {@link Configurator}. + * + * @param workspaceDir the temporary folder to use as workspace for this test. + * @throws Exception on error. + */ + @Test + // arrange + 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 setup = templates.resolve(IdeContext.FOLDER_SETUP); + Path update = templates.resolve(IdeContext.FOLDER_UPDATE); + merger.merge(setup, update, context.getVariables(), workspaceDir); + + // assert + Path mainPrefsFile = workspaceDir.resolve("main.prefs"); + Properties mainPrefs = PropertiesMerger.load(mainPrefsFile); + assertThat(mainPrefs).containsOnly(JAVA_VERSION, JAVA_HOME, THEME, UI); + Path jsonFolder = workspaceDir.resolve("json"); + assertThat(jsonFolder).isDirectory(); + assertThat(jsonFolder.resolve("settings.json")).hasContent(""" + + { + "java.home": "${IDE_HOME}/software/java", + "tslint.autoFixOnSave": true, + "object": { + "bar": "${IDE_HOME}/bar", + "array": [ + "a", + "b", + "${IDE_HOME}" + ], + "foo": "${IDE_HOME}/foo" + } + } + """.replace("${IDE_HOME}", IDE_HOME)); + assertThat(jsonFolder.resolve("update.json")).hasContent(""" + + { + "key": "value" + } + """); + + Path configFolder = workspaceDir.resolve("config"); + assertThat(configFolder).isDirectory(); + Path indentFile = configFolder.resolve("indent.properties"); + Properties indent = PropertiesMerger.load(indentFile); + assertThat(indent).containsOnly(INDENTATION); + assertThat(configFolder.resolve("layout.xml")).hasContent(""" + + navigator + debugger + editor + console + ${IDE_HOME} + + """.replace("${IDE_HOME}", IDE_HOME)); + + // and arrange + EDITOR.apply(mainPrefs); + JAVA_VERSION_HACKED.apply(mainPrefs); + UI_HACKED.apply(mainPrefs); + THEME_HACKED.apply(mainPrefs); + INDENTATION_HACKED.apply(mainPrefs); + PropertiesMerger.save(mainPrefs, mainPrefsFile); + + // act + merger.merge(setup, update, context.getVariables(), workspaceDir); + + // assert + mainPrefs = PropertiesMerger.load(mainPrefsFile); + assertThat(mainPrefs).containsOnly(JAVA_VERSION, JAVA_HOME, THEME_HACKED, UI_HACKED, EDITOR, INDENTATION_HACKED); + } + + private static class Prop implements Entry { + + private final String key; + + private String value; + + private Prop(String key, String value) { + + super(); + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + + return this.key; + } + + @Override + public String getValue() { + + return this.value; + } + + @Override + public String setValue(String value) { + + throw new IllegalStateException(value); + } + + public void apply(Properties properties) { + + properties.setProperty(this.key, this.value); + } + + @Override + public String toString() { + + return this.key + "=" + this.value; + } + + } + +} diff --git a/cli/src/test/resources/templates/setup/config/layout.xml b/cli/src/test/resources/templates/setup/config/layout.xml index d0d4c543e..f043b84d6 100644 --- a/cli/src/test/resources/templates/setup/config/layout.xml +++ b/cli/src/test/resources/templates/setup/config/layout.xml @@ -3,5 +3,5 @@ debugger editor console - ${client.env.home} + ${IDE_HOME} \ No newline at end of file diff --git a/cli/src/test/resources/templates/setup/json/settings.json b/cli/src/test/resources/templates/setup/json/settings.json index 544e9b684..ff26c4334 100644 --- a/cli/src/test/resources/templates/setup/json/settings.json +++ b/cli/src/test/resources/templates/setup/json/settings.json @@ -1,6 +1,6 @@ { "object": { - "foo": "${client.env.home}/foo", - "array": ["a", "b", "${client.env.home}"] + "foo": "${IDE_HOME}/foo", + "array": ["a", "b", "${IDE_HOME}"] } } \ No newline at end of file diff --git a/cli/src/test/resources/templates/update/config/merge.xml b/cli/src/test/resources/templates/update/config/merge.xml index ba71af59b..cddb0881c 100644 --- a/cli/src/test/resources/templates/update/config/merge.xml +++ b/cli/src/test/resources/templates/update/config/merge.xml @@ -4,5 +4,5 @@ value3 hello world! - ${client.env.home} + ${IDE_HOME} \ No newline at end of file diff --git a/cli/src/test/resources/templates/update/json/settings.json b/cli/src/test/resources/templates/update/json/settings.json index acdeb824b..655d052ae 100644 --- a/cli/src/test/resources/templates/update/json/settings.json +++ b/cli/src/test/resources/templates/update/json/settings.json @@ -1,8 +1,8 @@ { - "java.home": "${client.env.home}/software/java", + "java.home": "${IDE_HOME}/software/java", "tslint.autoFixOnSave": true, "object": { - "bar": "${client.env.home}/bar", - "array": ["a", "b", "${client.env.home}"] + "bar": "${IDE_HOME}/bar", + "array": ["a", "b", "${IDE_HOME}"] } } \ No newline at end of file diff --git a/cli/src/test/resources/templates/update/main.prefs b/cli/src/test/resources/templates/update/main.prefs index 9a13e5ea7..58cce161f 100644 --- a/cli/src/test/resources/templates/update/main.prefs +++ b/cli/src/test/resources/templates/update/main.prefs @@ -1,2 +1,2 @@ java.version=1.11 -java.home=${client.env.home}/software/java \ No newline at end of file +java.home=${IDE_HOME}/software/java \ No newline at end of file