Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/devonfw/IDEasy into feature/d…
Browse files Browse the repository at this point in the history
…evonfw#13-implement-ToolCommandlet-for-AWS-CLI

# Conflicts:
#	cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
#	cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
#	cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java
  • Loading branch information
MattesMrzik committed Jan 8, 2024
2 parents eaa85fc + 0e84aff commit 9f4e0ea
Show file tree
Hide file tree
Showing 28 changed files with 1,195 additions and 146 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public abstract class AbstractEnvironmentVariables implements EnvironmentVariabl
// Variable surrounded with "${" and "}" such as "${JAVA_HOME}" 1......2........
private static final Pattern VARIABLE_SYNTAX = Pattern.compile("(\\$\\{([^}]+)})");

private static final String SELF_REFERENCING_NOT_FOUND = "";

private static final int MAX_RECURSION = 9;

private static final String VARIABLE_PREFIX = "${";
Expand Down Expand Up @@ -161,36 +163,88 @@ public EnvironmentVariables resolved() {
@Override
public String resolve(String string, Object src) {

return resolve(string, src, 0, src, string);
return resolve(string, src, 0, src, string, this);
}

private String resolve(String value, Object src, int recursion, Object rootSrc, String rootValue) {
/**
* This method is called recursively. This allows you to resolve variables that are defined by other variables.
*
* @param value 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 src 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.
* @param recursion the current recursion level. This is used to interrupt endless recursion.
* @param rootSrc the root source where the {@link String} to resolve originates from.
* @param rootValue the root value to resolve.
* @param resolvedVars this is a reference to an object of {@link EnvironmentVariablesResolved} being the lowest level
* in the {@link EnvironmentVariablesType hierarchy} of variables. In case of a self-referencing variable
* {@code x} the resolving has to continue one level higher in the {@link EnvironmentVariablesType hierarchy}
* to avoid endless recursion. The {@link EnvironmentVariablesResolved} is then used if another variable
* {@code y} must be resolved, since resolving this variable has to again start at the lowest level. For
* example: For levels {@code l1, l2} with {@code l1 < l2} and {@code x=${x} foo} and {@code y=bar} defined at
* level {@code l1} and {@code x=test ${y}} defined at level {@code l2}, {@code x} is first resolved at level
* {@code l1} and then up the {@link EnvironmentVariablesType hierarchy} at {@code l2} to avoid endless
* recursion. However, {@code y} must be resolved starting from the lowest level in the
* {@link EnvironmentVariablesType hierarchy} and therefore {@link EnvironmentVariablesResolved} is used.
* @return the given {@link String} with the variables resolved.
*/
private String resolve(String value, Object src, int recursion, Object rootSrc, String rootValue,
AbstractEnvironmentVariables resolvedVars) {

if (value == null) {
return null;
}
if (recursion > MAX_RECURSION) {
throw new IllegalStateException("Reached maximum recursion resolving " + value + " for root valiable " + rootSrc
throw new IllegalStateException("Reached maximum recursion resolving " + value + " for root variable " + 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);
String variableValue = resolvedVars.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);
continue;
}
EnvironmentVariables lowestFound = findVariable(variableName);
boolean isNotSelfReferencing = lowestFound == null || !lowestFound.getFlat(variableName).equals(value);

if (isNotSelfReferencing) {
// looking for "variableName" starting from resolved upwards the hierarchy
String replacement = resolvedVars.resolve(variableValue, variableName, recursion, rootSrc, rootValue,
resolvedVars);
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
} else { // is self referencing
// finding next occurrence of "variableName" up the hierarchy of EnvironmentVariablesType
EnvironmentVariables next = lowestFound.getParent();
while (next != null) {
if (next.getFlat(variableName) != null) {
break;
}
next = next.getParent();
}
if (next == null) {
matcher.appendReplacement(sb, Matcher.quoteReplacement(SELF_REFERENCING_NOT_FOUND));
continue;
}
// resolving a self referencing variable one level up the hierarchy of EnvironmentVariablesType, i.e. at "next",
// to avoid endless recursion
String replacement = ((AbstractEnvironmentVariables) next).resolve(next.getFlat(variableName), variableName,
recursion, rootSrc, rootValue, resolvedVars);
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));

}
} while (matcher.find());
matcher.appendTail(sb);

String resolved = sb.toString();
return resolved;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ default EnvironmentVariables findVariable(String name) {
* @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.
* @return the given {@link String} with the variables resolved.
* @see com.devonfw.tools.ide.tool.ide.IdeToolCommandlet
*/
String resolve(String string, Object source);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,9 @@ public String set(String name, String value, boolean export) {
String oldValue = this.variables.put(name, value);
boolean flagChanged = export != this.exportedVariables.contains(name);
if (Objects.equals(value, oldValue) && !flagChanged) {
this.context.trace("Set valiable '{}={}' caused no change in {}", name, value, this.propertiesFilePath);
this.context.trace("Set variable '{}={}' caused no change in {}", name, value, this.propertiesFilePath);
} else {
this.context.debug("Set valiable '{}={}' in {}", name, value, this.propertiesFilePath);
this.context.debug("Set variable '{}={}' in {}", name, value, this.propertiesFilePath);
this.modifiedVariables.add(name);
if (export && (value != null)) {
this.exportedVariables.add(name);
Expand Down
15 changes: 14 additions & 1 deletion cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,24 @@ public interface FileAccess {
void move(Path source, Path targetDir);

/**
* @param source the source {@link Path} to link to.
* Creates a symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows
* junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, which must
* point to absolute paths. Therefore, the created link will be absolute instead of relative.
*
* @param source the source {@link Path} to link to, may be relative or absolute.
* @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}.
* @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute.
*/
void symlink(Path source, Path targetLink, boolean relative);

/**
* Creates a relative symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a
* Windows junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback,
* which must point to absolute paths. Therefore, the created link will be absolute instead of relative.
*
* @param source the source {@link Path} to link to, may be relative or absolute.
* @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}.
*/
default void symlink(Path source, Path targetLink) {

symlink(source, targetLink, true);
Expand Down
187 changes: 139 additions & 48 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -293,67 +293,158 @@ private void copyRecursive(Path source, Path target, FileCopyMode mode) throws I
}
}

@Override
public void symlink(Path source, Path targetLink, boolean relative) {
/**
* Deletes the given {@link Path} if it is a symbolic link or a Windows junction. And throws an
* {@link IllegalStateException} if there is a file at the given {@link Path} that is neither a symbolic link nor a
* Windows junction.
*
* @param path the {@link Path} to delete.
* @throws IOException if the actual {@link Files#delete(Path) deletion} fails.
*/
private void deleteLinkIfExists(Path path) throws IOException {

if (!source.isAbsolute()) {
throw new IllegalStateException("When creating a symbolic link the source (" + source
+ ") must be an absolute path. If you want to create a relative link, "
+ "then pass the source as an absolute path and set the flag \"relative\" to true.");
}
Path relativeSource = null;
if (relative) {
relativeSource = targetLink.getParent().relativize(source);
// to make relative links like this work: dir/link -> dir
relativeSource = (relativeSource.toString().isEmpty()) ? Paths.get(".") : relativeSource;
this.context.trace("Creating a relative symbolic link {} pointing to {}, with absolute path {}", targetLink,
relativeSource, source);
} else {
this.context.trace("Creating symbolic link {} pointing to {}", targetLink, source);
boolean exists = false;
boolean isJunction = false;
if (this.context.getSystemInfo().isWindows()) {
try { // since broken junctions are not detected by Files.exists(brokenJunction)
BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
exists = true;
isJunction = attr.isOther() && attr.isDirectory();
} catch (NoSuchFileException e) {
// ignore, since there is no previous file at the location, so nothing to delete
return;
}
}
exists = exists || Files.exists(path); // "||" since broken junctions are not detected by
// Files.exists(brokenJunction)
boolean isSymlink = exists && Files.isSymbolicLink(path);

try {
if (Files.exists(targetLink)) {
if (Files.isSymbolicLink(targetLink)) {
this.context.debug("Deleting symbolic link to be re-created at {}", targetLink);
Files.delete(targetLink);
}
assert !(isSymlink && isJunction);

if (exists) {
if (isJunction || isSymlink) {
this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
Files.delete(path);
} else {
throw new IllegalStateException(
"The file at " + path + " was not deleted since it is not a symlink or a Windows junction");
}
try { // since broken junctions are not detected by Files.exists(brokenJunction)
BasicFileAttributes attr = Files.readAttributes(targetLink, BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS);
if (attr.isOther() && attr.isDirectory() && this.context.getSystemInfo().isWindows()) {
this.context.debug("Deleting symbolic link (junction) to be re-created at {}", targetLink);
Files.delete(targetLink);
}
}

/**
* Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag.
* Additionally, {@link Path#toRealPath(LinkOption...)} is applied to {@code source}.
*
* @param source the {@link Path} to adapt.
* @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is
* set to {@code true}.
* @param relative the {@code relative} flag.
* @return the adapted {@link Path}.
* @see FileAccessImpl#symlink(Path, Path, boolean)
*/
private Path adaptPath(Path source, Path targetLink, boolean relative) throws IOException {

if (source.isAbsolute()) {
try {
source = source.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2
} catch (IOException e) {
throw new IOException(
"Calling toRealPath() on the source (" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
}
if (relative) {
source = targetLink.getParent().relativize(source);
// to make relative links like this work: dir/link -> dir
source = (source.toString().isEmpty()) ? Paths.get(".") : source;
}
} else { // source is relative
if (relative) {
// even though the source is already relative, toRealPath should be called to transform paths like
// this ../d1/../d2 to ../d2
source = targetLink.getParent()
.relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS));
source = (source.toString().isEmpty()) ? Paths.get(".") : source;
} else { // !relative
try {
source = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
} catch (IOException e) {
throw new IOException("Calling toRealPath() on " + targetLink + ".resolveSibling(" + source
+ ") in method FileAccessImpl.adaptPath() failed.", e);
}
} catch (NoSuchFileException e) {
// ignore, since there is no previous file at the location, which is fine
}
Files.createSymbolicLink(targetLink, relative ? relativeSource : source);
}
return source;
}

/**
* Creates a Windows junction at {@code targetLink} pointing to {@code source}.
*
* @param source must be another Windows junction or a directory.
* @param targetLink the location of the Windows junction.
*/
private void createWindowsJunction(Path source, Path targetLink) {

this.context.trace("Creating a Windows junction at " + targetLink + " with " + source + " as source.");
Path fallbackPath;
if (!source.isAbsolute()) {
this.context.warning(
"You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an "
+ "alternative, however, these can not point to relative paths. So the source (" + source
+ ") is interpreted as an absolute path.");
try {
fallbackPath = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
} catch (IOException e) {
throw new IllegalStateException(
"Since Windows junctions are used, the source must be an absolute path. The transformation of the passed "
+ "source (" + source + ") to an absolute path failed.",
e);
}

} else {
fallbackPath = source;
}
if (!Files.isDirectory(fallbackPath)) { // if source is a junction. This returns true as well.
throw new IllegalStateException(
"These junctions can only point to directories or other junctions. Please make sure that the source ("
+ fallbackPath + ") is one of these.");
}
this.context.newProcess().executable("cmd")
.addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), fallbackPath.toString()).run();
}

@Override
public void symlink(Path source, Path targetLink, boolean relative) {

Path adaptedSource = null;
try {
adaptedSource = adaptPath(source, targetLink, relative);
} catch (IOException e) {
throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink
+ ") and relative (" + relative + ")", e);
}
this.context.trace("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative",
targetLink, adaptedSource);

try {
deleteLinkIfExists(targetLink);
} catch (IOException e) {
throw new IllegalStateException("Failed to delete previous symlink or Windows junction at " + targetLink, e);
}

try {
Files.createSymbolicLink(targetLink, adaptedSource);
} catch (FileSystemException e) {
if (this.context.getSystemInfo().isWindows()) {
String infoMsg = "Due to lack of permissions, Microsoft's mklink with junction had to be used to create "
this.context.info("Due to lack of permissions, Microsoft's 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: " + e.getMessage();
this.context.info(infoMsg);
if (relative) {
this.context.warning(
"You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an "
+ "alternative, however, these can not point to relative paths. So the flag \"relative = true\" is ignored.");
}
if (!Files.isDirectory(source)) { // if source is a junction. This returns true as well.
throw new IllegalStateException(infoMsg
+ "\\n These junctions can only point to directories or other junctions. Please make sure that the source ("
+ source + ") is one of these.");
}
this.context.newProcess().executable("cmd")
.addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), source.toString()).run();
+ "further details. Error was: " + e.getMessage());
createWindowsJunction(adaptedSource, targetLink);
} else {
throw new RuntimeException(e);
}
} catch (IOException e) {
throw new IllegalStateException("Failed to create a symbolic link " + targetLink + " pointing to " + source
+ " with the flag \"relative\" set to " + relative, e);
throw new IllegalStateException("Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative")
+ "symbolic link " + targetLink + " pointing to " + source, e);
}
}

Expand Down
6 changes: 3 additions & 3 deletions cli/src/main/java/com/devonfw/tools/ide/tool/aws/Aws.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ protected void moveAndProcessExtraction(Path from, Path to) {
pc.addArgs("-i", from.toString(), "-b", from.toString());
pc.run();

// the install-script that aws ships creates symbolic links to binaries but using absolute paths
// since the current process happens in a temporary dir these links wouldn't be valid after moving the
// The install-script that aws ships creates symbolic links to binaries but using absolute paths.
// Since the current process happens in a temporary dir, these links wouldn't be valid after moving the
// installation files to the target dir. So the absolute paths are replaced by relative ones.
for (String file : new String[] { "aws", "aws_completer", Path.of("v2").resolve("current").toString() }) {
Path link = from.resolve(file);
try {
this.context.getFileAccess().symlink(link.toRealPath(), link);
this.context.getFileAccess().symlink(link.toRealPath(), link, true);
} catch (IOException e) {
throw new RuntimeException(
"Failed to replace absolute link (" + link + ") provided by AWS install script with relative link.", e);
Expand Down
Loading

0 comments on commit 9f4e0ea

Please sign in to comment.