• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 17668811506

12 Sep 2025 08:17AM UTC coverage: 68.712%. Remained the same
17668811506

push

github

web-flow
bugfix for Windows edge-case (#1465)

Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>

3379 of 5385 branches covered (62.75%)

Branch coverage included in aggregate %.

8818 of 12366 relevant lines covered (71.31%)

3.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.29
cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java
1
package com.devonfw.tools.ide.common;
2

3
import java.io.File;
4
import java.io.IOException;
5
import java.nio.file.Files;
6
import java.nio.file.LinkOption;
7
import java.nio.file.Path;
8
import java.util.ArrayList;
9
import java.util.Collections;
10
import java.util.HashMap;
11
import java.util.Iterator;
12
import java.util.List;
13
import java.util.Map;
14
import java.util.regex.Pattern;
15
import java.util.stream.Stream;
16

17
import com.devonfw.tools.ide.context.IdeContext;
18
import com.devonfw.tools.ide.os.SystemInfoImpl;
19
import com.devonfw.tools.ide.os.WindowsPathSyntax;
20
import com.devonfw.tools.ide.variable.IdeVariables;
21

22
/**
23
 * Represents the PATH variable in a structured way. The PATH contains the system path entries together with the entries for the IDEasy tools. The generic
24
 * system path entries are stored in a {@link List} ({@code paths}) and the tool entries are stored in a {@link Map} ({@code tool2pathMap}) as they can change
25
 * dynamically at runtime (e.g. if a new tool is installed). As the tools must have priority the actual PATH is build by first the entries for the tools and
26
 * then the generic entries from the system PATH. Such tool entries are ignored from the actual PATH of the {@link System#getenv(String) environment} at
27
 * construction time and are recomputed from the "software" folder. This is important as the initial {@link System#getenv(String) environment} PATH entries can
28
 * come from a different IDEasy project and the use may have changed projects before calling us again. Recomputing the PATH ensures side-effects from other
29
 * projects. However, it also will ensure all the entries to IDEasy locations are automatically managed and therefore cannot be managed manually be the
30
 * end-user.
31
 */
32
public class SystemPath {
33

34
  private static final Pattern REGEX_WINDOWS_PATH = Pattern.compile("([a-zA-Z]:)?(\\\\[a-zA-Z0-9\\s_.-]+)+\\\\?");
3✔
35

36
  private final String envPath;
37

38
  private final char pathSeparator;
39

40
  private final Map<String, Path> tool2pathMap;
41

42
  private final List<Path> paths;
43

44
  private final List<Path> extraPathEntries;
45

46
  private final IdeContext context;
47

48
  private static final List<String> EXTENSION_PRIORITY = List.of(".exe", ".cmd", ".bat", ".msi", ".ps1", "");
9✔
49

50
  /**
51
   * The constructor.
52
   *
53
   * @param context {@link IdeContext}.
54
   */
55
  public SystemPath(IdeContext context) {
56

57
    this(context, System.getenv(IdeVariables.PATH.getName()));
×
58
  }
×
59

60
  /**
61
   * The constructor.
62
   *
63
   * @param context {@link IdeContext}.
64
   * @param envPath the value of the PATH variable.
65
   */
66
  public SystemPath(IdeContext context, String envPath) {
67

68
    this(context, envPath, context.getIdeRoot(), context.getSoftwarePath());
8✔
69
  }
1✔
70

71
  /**
72
   * The constructor.
73
   *
74
   * @param context {@link IdeContext} for the output of information.
75
   * @param envPath the value of the PATH variable.
76
   * @param ideRoot the {@link IdeContext#getIdeRoot() IDE_ROOT}.
77
   * @param softwarePath the {@link IdeContext#getSoftwarePath() software path}.
78
   */
79
  public SystemPath(IdeContext context, String envPath, Path ideRoot, Path softwarePath) {
80

81
    this(context, envPath, ideRoot, softwarePath, File.pathSeparatorChar, Collections.emptyList());
8✔
82
  }
1✔
83

84
  /**
85
   * The constructor.
86
   *
87
   * @param context {@link IdeContext} for the output of information.
88
   * @param envPath the value of the PATH variable.
89
   * @param ideRoot the {@link IdeContext#getIdeRoot() IDE_ROOT}.
90
   * @param softwarePath the {@link IdeContext#getSoftwarePath() software path}.
91
   * @param pathSeparator the path separator char (';' for Windows and ':' otherwise).
92
   * @param extraPathEntries the {@link List} of additional {@link Path}s to prepend.
93
   */
94
  public SystemPath(IdeContext context, String envPath, Path ideRoot, Path softwarePath, char pathSeparator, List<Path> extraPathEntries) {
95

96
    this(context, envPath, pathSeparator, extraPathEntries, new HashMap<>(), new ArrayList<>());
12✔
97
    String[] envPaths = envPath.split(Character.toString(pathSeparator));
5✔
98
    for (String segment : envPaths) {
16✔
99
      Path path = Path.of(segment);
5✔
100
      String tool = getTool(path, ideRoot);
4✔
101
      if (tool == null) {
2!
102
        this.paths.add(path);
5✔
103
      }
104
    }
105
    collectToolPath(softwarePath);
3✔
106
  }
1✔
107

108
  private SystemPath(IdeContext context, String envPath, char pathSeparator, List<Path> extraPathEntries, Map<String, Path> tool2PathMap, List<Path> paths) {
109

110
    super();
2✔
111
    this.context = context;
3✔
112
    this.envPath = envPath;
3✔
113
    this.pathSeparator = pathSeparator;
3✔
114
    this.extraPathEntries = extraPathEntries;
3✔
115
    this.tool2pathMap = tool2PathMap;
3✔
116
    this.paths = paths;
3✔
117
  }
1✔
118

119
  private void collectToolPath(Path softwarePath) {
120

121
    if (softwarePath == null) {
2✔
122
      return;
1✔
123
    }
124
    if (Files.isDirectory(softwarePath)) {
5✔
125
      try (Stream<Path> children = Files.list(softwarePath)) {
3✔
126
        Iterator<Path> iterator = children.iterator();
3✔
127
        while (iterator.hasNext()) {
3✔
128
          Path child = iterator.next();
4✔
129
          String tool = child.getFileName().toString();
4✔
130
          if (!"extra".equals(tool) && Files.isDirectory(child)) {
9!
131
            Path toolPath = child;
2✔
132
            Path bin = child.resolve("bin");
4✔
133
            if (Files.isDirectory(bin)) {
5✔
134
              toolPath = bin;
2✔
135
            }
136
            this.tool2pathMap.put(tool, toolPath);
6✔
137
          }
138
        }
1✔
139
      } catch (IOException e) {
×
140
        throw new IllegalStateException("Failed to list children of " + softwarePath, e);
×
141
      }
1✔
142
    }
143
  }
1✔
144

145
  private static String getTool(Path path, Path ideRoot) {
146

147
    if (ideRoot == null) {
2✔
148
      return null;
2✔
149
    }
150
    if (path.startsWith(ideRoot)) {
4✔
151
      Path relativized = ideRoot.relativize(path);
4✔
152
      int count = relativized.getNameCount();
3✔
153
      if (count >= 3) {
3!
154
        if (relativized.getName(1).toString().equals("software")) {
×
155
          return relativized.getName(2).toString();
×
156
        }
157
      }
158
    }
159
    return null;
2✔
160
  }
161

162
  private Path findBinaryInOrder(Path path, String tool) {
163

164
    List<String> extensionPriority = List.of("");
3✔
165
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
166
      extensionPriority = EXTENSION_PRIORITY;
×
167
    }
168
    for (String extension : extensionPriority) {
10✔
169

170
      Path fileToExecute = path.resolve(tool + extension);
6✔
171

172
      if (Files.exists(fileToExecute, LinkOption.NOFOLLOW_LINKS)) {
9✔
173
        return fileToExecute;
2✔
174
      }
175
    }
1✔
176

177
    return null;
2✔
178
  }
179

180
  /**
181
   * @param binaryName the name of the tool.
182
   * @return {@code true} if the given {@code tool} is a binary that can be found on the PATH, {@code false} otherwise.
183
   */
184
  public boolean hasBinaryOnPath(String binaryName) {
185
    Path binary = Path.of(binaryName);
×
186
    Path resolvedBinary = findBinary(binary);
×
187
    return (resolvedBinary != binary);
×
188
  }
189

190
  /**
191
   * @param binaryName the name of the tool.
192
   * @return the {@link Path} to the binary executable of the tool. E.g. if "mvn" is given then ".../software/mvn/bin/mvn" could be returned. If the executable
193
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
194
   */
195
  public Path findBinaryPathByName(String binaryName) {
196
    return findBinary(Path.of(binaryName));
×
197
  }
198

199
  /**
200
   * @param toolPath the {@link Path} to the tool installation.
201
   * @return the {@link Path} to the binary executable of the tool. E.g. if "mvn" is given then ".../software/mvn/bin/mvn" could be returned. If the executable
202
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
203
   */
204
  public Path findBinary(Path toolPath) {
205

206
    Path parent = toolPath.getParent();
3✔
207
    String fileName = toolPath.getFileName().toString();
4✔
208

209
    if (parent == null) {
2✔
210

211
      for (Path path : this.tool2pathMap.values()) {
12✔
212
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
213
        if (binaryPath != null) {
2✔
214
          return binaryPath;
2✔
215
        }
216
      }
1✔
217

218
      for (Path path : this.paths) {
11!
219
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
220
        if (binaryPath != null) {
2✔
221
          return binaryPath;
2✔
222
        }
223
      }
1✔
224
    } else {
225
      Path binaryPath = findBinaryInOrder(parent, fileName);
5✔
226
      if (binaryPath != null) {
2✔
227
        return binaryPath;
2✔
228
      }
229
    }
230

231
    return toolPath;
2✔
232
  }
233

234
  /**
235
   * @param tool the name of the tool.
236
   * @return the {@link Path} to the directory of the tool where the binaries can be found or {@code null} if the tool is not installed.
237
   */
238
  public Path getPath(String tool) {
239

240
    return this.tool2pathMap.get(tool);
6✔
241
  }
242

243
  /**
244
   * @param tool the name of the tool.
245
   * @param path the new {@link #getPath(String) tool bin path}.
246
   */
247
  public void setPath(String tool, Path path) {
248

249
    this.tool2pathMap.put(tool, path);
6✔
250
  }
1✔
251

252
  @Override
253
  public String toString() {
254

255
    return toString(null);
4✔
256
  }
257

258
  /**
259
   * @param pathSyntax the {@link WindowsPathSyntax} to convert to.
260
   * @return this {@link SystemPath} as {@link String} for the PATH environment variable.
261
   */
262
  public String toString(WindowsPathSyntax pathSyntax) {
263

264
    char separator;
265
    if (pathSyntax == WindowsPathSyntax.MSYS) {
3!
266
      separator = ':';
×
267
    } else {
268
      separator = this.pathSeparator;
3✔
269
    }
270
    StringBuilder sb = new StringBuilder(this.envPath.length() + 128);
9✔
271
    for (Path path : this.extraPathEntries) {
11✔
272
      appendPath(path, sb, separator, pathSyntax);
5✔
273
    }
1✔
274
    for (Path path : this.tool2pathMap.values()) {
12✔
275
      appendPath(path, sb, separator, pathSyntax);
5✔
276
    }
1✔
277
    for (Path path : this.paths) {
11✔
278
      appendPath(path, sb, separator, pathSyntax);
5✔
279
    }
1✔
280
    return sb.toString();
3✔
281
  }
282

283
  /**
284
   * Derive a new {@link SystemPath} from this instance with the given parameters.
285
   *
286
   * @param overriddenPath the entire PATH to override and replace the current one from this {@link SystemPath} or {@code null} to keep the current PATH.
287
   * @param extraPathEntries the {@link List} of additional PATH entries to add to the beginning of the PATH. May be empty to add nothing.
288
   * @return the new {@link SystemPath} derived from this instance with the given parameters applied.
289
   */
290
  public SystemPath withPath(String overriddenPath, List<Path> extraPathEntries) {
291

292
    if (overriddenPath == null) {
2!
293
      return new SystemPath(this.context, this.envPath, this.pathSeparator, extraPathEntries, this.tool2pathMap, this.paths);
15✔
294
    } else {
295
      return new SystemPath(this.context, overriddenPath, null, null, this.pathSeparator, extraPathEntries);
×
296
    }
297
  }
298

299
  private static void appendPath(Path path, StringBuilder sb, char separator, WindowsPathSyntax pathSyntax) {
300

301
    if (sb.length() > 0) {
3✔
302
      sb.append(separator);
4✔
303
    }
304
    String pathString;
305
    if (pathSyntax == null) {
2✔
306
      pathString = path.toString();
4✔
307
    } else {
308
      pathString = pathSyntax.format(path);
4✔
309
    }
310
    sb.append(pathString);
4✔
311
  }
1✔
312

313
  /**
314
   * Method to validate if a given path string is a Windows path or not
315
   *
316
   * @param pathString The string to check if it is a Windows path string.
317
   * @return {@code true} if it is a valid windows path string, else {@code false}.
318
   */
319
  public static boolean isValidWindowsPath(String pathString) {
320

321
    return REGEX_WINDOWS_PATH.matcher(pathString).matches();
5✔
322
  }
323
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc