Skip to content

Commit b030586

Browse files
authored
Merge branch 'main' into 100-implement-repository-commandlet
2 parents 21f14df + 0e84aff commit b030586

32 files changed

+1437
-42
lines changed

.github/workflows/build-pr.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ jobs:
1414
java-version: '17'
1515
- name: Build project with Maven
1616
run: mvn -B -ntp -Dstyle.color=always install
17+
- name: Coveralls GitHub Action
18+
uses: coverallsapp/[email protected]

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ jobs:
2323
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
2424
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
2525
run: mvn --settings .mvn/settings.xml -DskipTests=true -Darchetype.test.skip=true -Dmaven.install.skip=true -Dstyle.color=always -B -ntp deploy
26+
- name: Coveralls GitHub Action
27+
uses: coverallsapp/[email protected]

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ image:https://img.shields.io/github/license/devonfw/IDEasy.svg?label=License["Ap
1010
image:https://img.shields.io/maven-central/v/com.devonfw.tools.ide/ide-cli.svg?label=Maven%20Central["Maven Central",link=https://search.maven.org/search?q=g:com.devonfw.tools.ide]
1111
image:https://github.com/devonfw/IDEasy/actions/workflows/build.yml/badge.svg["Build Status",link="https://github.com/devonfw/IDEasy/actions/workflows/build.yml"]
1212
image:https://github.com/devonfw/IDEasy/actions/workflows/update-urls.yml/badge.svg["Update URLS Status",link="https://github.com/devonfw/IDEasy/actions/workflows/update-urls.yml"]
13+
image:https://coveralls.io/repos/github/devonfw/IDEasy/badge.svg?branch=main["Coverage Status",link="https://coveralls.io/github/devonfw/IDEasy?branch=main"]
1314

1415
toc::[]
1516

cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public abstract class AbstractEnvironmentVariables implements EnvironmentVariabl
2828
// Variable surrounded with "${" and "}" such as "${JAVA_HOME}" 1......2........
2929
private static final Pattern VARIABLE_SYNTAX = Pattern.compile("(\\$\\{([^}]+)})");
3030

31+
private static final String SELF_REFERENCING_NOT_FOUND = "";
32+
3133
private static final int MAX_RECURSION = 9;
3234

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

164-
return resolve(string, src, 0, src, string);
166+
return resolve(string, src, 0, src, string, this);
165167
}
166168

167-
private String resolve(String value, Object src, int recursion, Object rootSrc, String rootValue) {
169+
/**
170+
* This method is called recursively. This allows you to resolve variables that are defined by other variables.
171+
*
172+
* @param value the {@link String} that potentially contains variables in the syntax "${«variable«}". Those will be
173+
* resolved by this method and replaced with their {@link #get(String) value}.
174+
* @param src the source where the {@link String} to resolve originates from. Should have a reasonable
175+
* {@link Object#toString() string representation} that will be used in error or log messages if a variable
176+
* could not be resolved.
177+
* @param recursion the current recursion level. This is used to interrupt endless recursion.
178+
* @param rootSrc the root source where the {@link String} to resolve originates from.
179+
* @param rootValue the root value to resolve.
180+
* @param resolvedVars this is a reference to an object of {@link EnvironmentVariablesResolved} being the lowest level
181+
* in the {@link EnvironmentVariablesType hierarchy} of variables. In case of a self-referencing variable
182+
* {@code x} the resolving has to continue one level higher in the {@link EnvironmentVariablesType hierarchy}
183+
* to avoid endless recursion. The {@link EnvironmentVariablesResolved} is then used if another variable
184+
* {@code y} must be resolved, since resolving this variable has to again start at the lowest level. For
185+
* example: For levels {@code l1, l2} with {@code l1 < l2} and {@code x=${x} foo} and {@code y=bar} defined at
186+
* level {@code l1} and {@code x=test ${y}} defined at level {@code l2}, {@code x} is first resolved at level
187+
* {@code l1} and then up the {@link EnvironmentVariablesType hierarchy} at {@code l2} to avoid endless
188+
* recursion. However, {@code y} must be resolved starting from the lowest level in the
189+
* {@link EnvironmentVariablesType hierarchy} and therefore {@link EnvironmentVariablesResolved} is used.
190+
* @return the given {@link String} with the variables resolved.
191+
*/
192+
private String resolve(String value, Object src, int recursion, Object rootSrc, String rootValue,
193+
AbstractEnvironmentVariables resolvedVars) {
168194

169195
if (value == null) {
170196
return null;
171197
}
172198
if (recursion > MAX_RECURSION) {
173-
throw new IllegalStateException("Reached maximum recursion resolving " + value + " for root valiable " + rootSrc
199+
throw new IllegalStateException("Reached maximum recursion resolving " + value + " for root variable " + rootSrc
174200
+ " with value '" + rootValue + "'.");
175201
}
176202
recursion++;
203+
177204
Matcher matcher = VARIABLE_SYNTAX.matcher(value);
178205
if (!matcher.find()) {
179206
return value;
180207
}
181208
StringBuilder sb = new StringBuilder(value.length() + EXTRA_CAPACITY);
182209
do {
183210
String variableName = matcher.group(2);
184-
String variableValue = getValue(variableName);
211+
String variableValue = resolvedVars.getValue(variableName);
185212
if (variableValue == null) {
186213
this.context.warning("Undefined variable {} in '{}={}' for root '{}={}'", variableName, src, value, rootSrc,
187214
rootValue);
188-
} else {
189-
String replacement = resolve(variableValue, variableName, recursion, rootSrc, rootValue);
215+
continue;
216+
}
217+
EnvironmentVariables lowestFound = findVariable(variableName);
218+
boolean isNotSelfReferencing = lowestFound == null || !lowestFound.getFlat(variableName).equals(value);
219+
220+
if (isNotSelfReferencing) {
221+
// looking for "variableName" starting from resolved upwards the hierarchy
222+
String replacement = resolvedVars.resolve(variableValue, variableName, recursion, rootSrc, rootValue,
223+
resolvedVars);
224+
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
225+
} else { // is self referencing
226+
// finding next occurrence of "variableName" up the hierarchy of EnvironmentVariablesType
227+
EnvironmentVariables next = lowestFound.getParent();
228+
while (next != null) {
229+
if (next.getFlat(variableName) != null) {
230+
break;
231+
}
232+
next = next.getParent();
233+
}
234+
if (next == null) {
235+
matcher.appendReplacement(sb, Matcher.quoteReplacement(SELF_REFERENCING_NOT_FOUND));
236+
continue;
237+
}
238+
// resolving a self referencing variable one level up the hierarchy of EnvironmentVariablesType, i.e. at "next",
239+
// to avoid endless recursion
240+
String replacement = ((AbstractEnvironmentVariables) next).resolve(next.getFlat(variableName), variableName,
241+
recursion, rootSrc, rootValue, resolvedVars);
190242
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
243+
191244
}
192245
} while (matcher.find());
193246
matcher.appendTail(sb);
247+
194248
String resolved = sb.toString();
195249
return resolved;
196250
}

cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ default EnvironmentVariables findVariable(String name) {
186186
* @param source the source where the {@link String} to resolve originates from. Should have a reasonable
187187
* {@link Object#toString() string representation} that will be used in error or log messages if a variable
188188
* could not be resolved.
189-
* @return the the given {@link String} with the variables resolved.
189+
* @return the given {@link String} with the variables resolved.
190190
* @see com.devonfw.tools.ide.tool.ide.IdeToolCommandlet
191191
*/
192192
String resolve(String string, Object source);

cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,9 @@ public String set(String name, String value, boolean export) {
222222
String oldValue = this.variables.put(name, value);
223223
boolean flagChanged = export != this.exportedVariables.contains(name);
224224
if (Objects.equals(value, oldValue) && !flagChanged) {
225-
this.context.trace("Set valiable '{}={}' caused no change in {}", name, value, this.propertiesFilePath);
225+
this.context.trace("Set variable '{}={}' caused no change in {}", name, value, this.propertiesFilePath);
226226
} else {
227-
this.context.debug("Set valiable '{}={}' in {}", name, value, this.propertiesFilePath);
227+
this.context.debug("Set variable '{}={}' in {}", name, value, this.propertiesFilePath);
228228
this.modifiedVariables.add(name);
229229
if (export && (value != null)) {
230230
this.exportedVariables.add(name);

cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,28 @@ public interface FileAccess {
6060
void move(Path source, Path targetDir);
6161

6262
/**
63-
* @param source the source {@link Path} to link to.
63+
* Creates a symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows
64+
* junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, which must
65+
* point to absolute paths. Therefore, the created link will be absolute instead of relative.
66+
*
67+
* @param source the source {@link Path} to link to, may be relative or absolute.
6468
* @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}.
69+
* @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute.
6570
*/
66-
void symlink(Path source, Path targetLink);
71+
void symlink(Path source, Path targetLink, boolean relative);
72+
73+
/**
74+
* Creates a relative symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a
75+
* Windows junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback,
76+
* which must point to absolute paths. Therefore, the created link will be absolute instead of relative.
77+
*
78+
* @param source the source {@link Path} to link to, may be relative or absolute.
79+
* @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}.
80+
*/
81+
default void symlink(Path source, Path targetLink) {
82+
83+
symlink(source, targetLink, true);
84+
}
6785

6886
/**
6987
* @param source the source {@link Path file or folder} to copy.

cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java

Lines changed: 147 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
import java.net.http.HttpResponse;
1414
import java.nio.file.FileSystemException;
1515
import java.nio.file.Files;
16+
import java.nio.file.LinkOption;
17+
import java.nio.file.NoSuchFileException;
1618
import java.nio.file.Path;
1719
import java.nio.file.Paths;
20+
import java.nio.file.attribute.BasicFileAttributes;
1821
import java.security.DigestInputStream;
1922
import java.security.MessageDigest;
2023
import java.security.NoSuchAlgorithmException;
@@ -290,28 +293,158 @@ private void copyRecursive(Path source, Path target, FileCopyMode mode) throws I
290293
}
291294
}
292295

296+
/**
297+
* Deletes the given {@link Path} if it is a symbolic link or a Windows junction. And throws an
298+
* {@link IllegalStateException} if there is a file at the given {@link Path} that is neither a symbolic link nor a
299+
* Windows junction.
300+
*
301+
* @param path the {@link Path} to delete.
302+
* @throws IOException if the actual {@link Files#delete(Path) deletion} fails.
303+
*/
304+
private void deleteLinkIfExists(Path path) throws IOException {
305+
306+
boolean exists = false;
307+
boolean isJunction = false;
308+
if (this.context.getSystemInfo().isWindows()) {
309+
try { // since broken junctions are not detected by Files.exists(brokenJunction)
310+
BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
311+
exists = true;
312+
isJunction = attr.isOther() && attr.isDirectory();
313+
} catch (NoSuchFileException e) {
314+
// ignore, since there is no previous file at the location, so nothing to delete
315+
return;
316+
}
317+
}
318+
exists = exists || Files.exists(path); // "||" since broken junctions are not detected by
319+
// Files.exists(brokenJunction)
320+
boolean isSymlink = exists && Files.isSymbolicLink(path);
321+
322+
assert !(isSymlink && isJunction);
323+
324+
if (exists) {
325+
if (isJunction || isSymlink) {
326+
this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
327+
Files.delete(path);
328+
} else {
329+
throw new IllegalStateException(
330+
"The file at " + path + " was not deleted since it is not a symlink or a Windows junction");
331+
}
332+
}
333+
}
334+
335+
/**
336+
* Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag.
337+
* Additionally, {@link Path#toRealPath(LinkOption...)} is applied to {@code source}.
338+
*
339+
* @param source the {@link Path} to adapt.
340+
* @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is
341+
* set to {@code true}.
342+
* @param relative the {@code relative} flag.
343+
* @return the adapted {@link Path}.
344+
* @see FileAccessImpl#symlink(Path, Path, boolean)
345+
*/
346+
private Path adaptPath(Path source, Path targetLink, boolean relative) throws IOException {
347+
348+
if (source.isAbsolute()) {
349+
try {
350+
source = source.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2
351+
} catch (IOException e) {
352+
throw new IOException(
353+
"Calling toRealPath() on the source (" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
354+
}
355+
if (relative) {
356+
source = targetLink.getParent().relativize(source);
357+
// to make relative links like this work: dir/link -> dir
358+
source = (source.toString().isEmpty()) ? Paths.get(".") : source;
359+
}
360+
} else { // source is relative
361+
if (relative) {
362+
// even though the source is already relative, toRealPath should be called to transform paths like
363+
// this ../d1/../d2 to ../d2
364+
source = targetLink.getParent()
365+
.relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS));
366+
source = (source.toString().isEmpty()) ? Paths.get(".") : source;
367+
} else { // !relative
368+
try {
369+
source = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
370+
} catch (IOException e) {
371+
throw new IOException("Calling toRealPath() on " + targetLink + ".resolveSibling(" + source
372+
+ ") in method FileAccessImpl.adaptPath() failed.", e);
373+
}
374+
}
375+
}
376+
return source;
377+
}
378+
379+
/**
380+
* Creates a Windows junction at {@code targetLink} pointing to {@code source}.
381+
*
382+
* @param source must be another Windows junction or a directory.
383+
* @param targetLink the location of the Windows junction.
384+
*/
385+
private void createWindowsJunction(Path source, Path targetLink) {
386+
387+
this.context.trace("Creating a Windows junction at " + targetLink + " with " + source + " as source.");
388+
Path fallbackPath;
389+
if (!source.isAbsolute()) {
390+
this.context.warning(
391+
"You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an "
392+
+ "alternative, however, these can not point to relative paths. So the source (" + source
393+
+ ") is interpreted as an absolute path.");
394+
try {
395+
fallbackPath = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
396+
} catch (IOException e) {
397+
throw new IllegalStateException(
398+
"Since Windows junctions are used, the source must be an absolute path. The transformation of the passed "
399+
+ "source (" + source + ") to an absolute path failed.",
400+
e);
401+
}
402+
403+
} else {
404+
fallbackPath = source;
405+
}
406+
if (!Files.isDirectory(fallbackPath)) { // if source is a junction. This returns true as well.
407+
throw new IllegalStateException(
408+
"These junctions can only point to directories or other junctions. Please make sure that the source ("
409+
+ fallbackPath + ") is one of these.");
410+
}
411+
this.context.newProcess().executable("cmd")
412+
.addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), fallbackPath.toString()).run();
413+
}
414+
293415
@Override
294-
public void symlink(Path source, Path targetLink) {
416+
public void symlink(Path source, Path targetLink, boolean relative) {
295417

296-
this.context.trace("Creating symbolic link {} pointing to {}", targetLink, source);
418+
Path adaptedSource = null;
297419
try {
298-
if (Files.exists(targetLink) && Files.isSymbolicLink(targetLink)) {
299-
this.context.debug("Deleting symbolic link to be re-created at {}", targetLink);
300-
Files.delete(targetLink);
301-
}
302-
Files.createSymbolicLink(targetLink, source);
420+
adaptedSource = adaptPath(source, targetLink, relative);
421+
} catch (IOException e) {
422+
throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink
423+
+ ") and relative (" + relative + ")", e);
424+
}
425+
this.context.trace("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative",
426+
targetLink, adaptedSource);
427+
428+
try {
429+
deleteLinkIfExists(targetLink);
430+
} catch (IOException e) {
431+
throw new IllegalStateException("Failed to delete previous symlink or Windows junction at " + targetLink, e);
432+
}
433+
434+
try {
435+
Files.createSymbolicLink(targetLink, adaptedSource);
303436
} catch (FileSystemException e) {
304437
if (this.context.getSystemInfo().isWindows()) {
305-
this.context.info(
306-
"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: "
307-
+ e.getMessage());
308-
this.context.newProcess().executable("cmd")
309-
.addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), source.toString()).run();
438+
this.context.info("Due to lack of permissions, Microsoft's mklink with junction had to be used to create "
439+
+ "a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for "
440+
+ "further details. Error was: " + e.getMessage());
441+
createWindowsJunction(adaptedSource, targetLink);
310442
} else {
311443
throw new RuntimeException(e);
312444
}
313445
} catch (IOException e) {
314-
throw new IllegalStateException("Failed to create a symbolic link " + targetLink + " pointing to " + source, e);
446+
throw new IllegalStateException("Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative")
447+
+ "symbolic link " + targetLink + " pointing to " + source, e);
315448
}
316449
}
317450

@@ -398,8 +531,7 @@ public void delete(Path path) {
398531
try {
399532
if (Files.isSymbolicLink(path)) {
400533
Files.delete(path);
401-
}
402-
else {
534+
} else {
403535
deleteRecursive(path);
404536
}
405537
} catch (IOException e) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.devonfw.tools.ide.version;
2+
3+
/**
4+
* Enum representing the type of interval regarding its boundaries.
5+
*/
6+
public enum BoundaryType {
7+
8+
/** Closed interval - includes the specified values at the boundaries. */
9+
CLOSED,
10+
11+
/** Open interval - excludes the specified values at the boundaries. */
12+
OPEN,
13+
14+
/** Left open interval - excludes the lower bound but includes the upper bound. */
15+
LEFT_OPEN,
16+
17+
/** Right open interval - includes the lower bound but excludes the upper bound. */
18+
RIGHT_OPEN
19+
}

0 commit comments

Comments
 (0)