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

devonfw / IDEasy / 19339509159

13 Nov 2025 05:03PM UTC coverage: 68.943% (-0.01%) from 68.955%
19339509159

Pull #1592

github

web-flow
Merge ff512b5a9 into 8ba76dc5f
Pull Request #1592: #1590: Add SystemPath.findBinary(Path toolPath, Predicate<Path> filter)

3499 of 5563 branches covered (62.9%)

Branch coverage included in aggregate %.

9163 of 12803 relevant lines covered (71.57%)

3.15 hits per line

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

84.36
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.Objects;
15
import java.util.function.Predicate;
16
import java.util.regex.Pattern;
17
import java.util.stream.Stream;
18

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

24
/**
25
 * 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
26
 * 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
27
 * 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
28
 * 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
29
 * construction time and are recomputed from the "software" folder. This is important as the initial {@link System#getenv(String) environment} PATH entries can
30
 * 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
31
 * projects. However, it also will ensure all the entries to IDEasy locations are automatically managed and therefore cannot be managed manually be the
32
 * end-user.
33
 */
34
public class SystemPath {
35

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

38
  private final String envPath;
39

40
  private final char pathSeparator;
41

42
  private final Map<String, Path> tool2pathMap;
43

44
  private final List<Path> paths;
45

46
  private final List<Path> extraPathEntries;
47

48
  private final IdeContext context;
49

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

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

59
    this(context, System.getenv(IdeVariables.PATH.getName()));
×
60
  }
×
61

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

70
    this(context, envPath, context.getIdeRoot(), context.getSoftwarePath());
8✔
71
  }
1✔
72

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

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

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

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

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

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

121
  private void collectToolPath(Path softwarePath) {
122

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

147
  private static String getTool(Path path, Path ideRoot) {
148

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

164
  private Path findBinaryInOrder(Path path, String tool) {
165

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

172
      Path fileToExecute = path.resolve(tool + extension);
6✔
173

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

179
    return null;
2✔
180
  }
181

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

192
  /**
193
   * @param binaryName the name of the tool.
194
   * @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
195
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
196
   */
197
  public Path findBinaryPathByName(String binaryName) {
198
    return findBinary(Path.of(binaryName));
×
199
  }
200

201
  /**
202
   * @param toolPath the {@link Path} to the tool installation.
203
   * @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
204
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
205
   */
206
  public Path findBinary(Path toolPath) {
207
    return findBinary(toolPath, p -> true);
7✔
208
  }
209

210
  /**
211
   * Finds a binary for {@code toolPath} but allows excluding candidates via {@code filter}. If the filter rejects a candidate, the search continues. If no
212
   * acceptable candidate is found, the original {@code toolPath} is returned unchanged.
213
   *
214
   * @param toolPath the {@link Path} to the tool installation or a simple name (e.g., "git").
215
   * @param filter a predicate that must return {@code true} for acceptable candidates; {@code false} rejects and continues searching.
216
   * @return the first acceptable {@link Path} found; if none, the original {@code toolPath}.
217
   */
218
  public Path findBinary(Path toolPath, Predicate<Path> filter) {
219
    Objects.requireNonNull(toolPath, "toolPath");
4✔
220
    Objects.requireNonNull(filter, "filter");
4✔
221

222
    Path parent = toolPath.getParent();
3✔
223
    String fileName = toolPath.getFileName().toString();
4✔
224

225
    if (parent == null) {
2✔
226
      for (Path path : this.tool2pathMap.values()) {
12✔
227
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
228
        if (binaryPath != null && filter.test(binaryPath)) {
6!
229
          return binaryPath;
2✔
230
        }
231
      }
1✔
232
      for (Path path : this.paths) {
11!
233
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
234
        if (binaryPath != null && filter.test(binaryPath)) {
6!
235
          return binaryPath;
2✔
236
        }
237
      }
1✔
238
    } else {
239
      Path binaryPath = findBinaryInOrder(parent, fileName);
5✔
240
      if (binaryPath != null && filter.test(binaryPath)) {
6!
241
        return binaryPath;
2✔
242
      }
243
    }
244

245
    return toolPath;
2✔
246
  }
247

248
  /**
249
   * @param tool the name of the tool.
250
   * @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.
251
   */
252
  public Path getPath(String tool) {
253

254
    return this.tool2pathMap.get(tool);
6✔
255
  }
256

257
  /**
258
   * @param tool the name of the tool.
259
   * @param path the new {@link #getPath(String) tool bin path}.
260
   */
261
  public void setPath(String tool, Path path) {
262

263
    this.tool2pathMap.put(tool, path);
6✔
264
  }
1✔
265

266
  @Override
267
  public String toString() {
268

269
    return toString(null);
4✔
270
  }
271

272
  /**
273
   * @param pathSyntax the {@link WindowsPathSyntax} to convert to.
274
   * @return this {@link SystemPath} as {@link String} for the PATH environment variable.
275
   */
276
  public String toString(WindowsPathSyntax pathSyntax) {
277

278
    char separator;
279
    if (pathSyntax == WindowsPathSyntax.MSYS) {
3!
280
      separator = ':';
×
281
    } else {
282
      separator = this.pathSeparator;
3✔
283
    }
284
    StringBuilder sb = new StringBuilder(this.envPath.length() + 128);
9✔
285
    for (Path path : this.extraPathEntries) {
11✔
286
      appendPath(path, sb, separator, pathSyntax);
5✔
287
    }
1✔
288
    for (Path path : this.tool2pathMap.values()) {
12✔
289
      appendPath(path, sb, separator, pathSyntax);
5✔
290
    }
1✔
291
    for (Path path : this.paths) {
11✔
292
      appendPath(path, sb, separator, pathSyntax);
5✔
293
    }
1✔
294
    return sb.toString();
3✔
295
  }
296

297
  /**
298
   * Derive a new {@link SystemPath} from this instance with the given parameters.
299
   *
300
   * @param overriddenPath the entire PATH to override and replace the current one from this {@link SystemPath} or {@code null} to keep the current PATH.
301
   * @param extraPathEntries the {@link List} of additional PATH entries to add to the beginning of the PATH. May be empty to add nothing.
302
   * @return the new {@link SystemPath} derived from this instance with the given parameters applied.
303
   */
304
  public SystemPath withPath(String overriddenPath, List<Path> extraPathEntries) {
305

306
    if (overriddenPath == null) {
2!
307
      return new SystemPath(this.context, this.envPath, this.pathSeparator, extraPathEntries, this.tool2pathMap, this.paths);
15✔
308
    } else {
309
      return new SystemPath(this.context, overriddenPath, null, null, this.pathSeparator, extraPathEntries);
×
310
    }
311
  }
312

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

315
    if (sb.length() > 0) {
3✔
316
      sb.append(separator);
4✔
317
    }
318
    String pathString;
319
    if (pathSyntax == null) {
2✔
320
      pathString = path.toString();
4✔
321
    } else {
322
      pathString = pathSyntax.format(path);
4✔
323
    }
324
    sb.append(pathString);
4✔
325
  }
1✔
326

327
  /**
328
   * Method to validate if a given path string is a Windows path or not
329
   *
330
   * @param pathString The string to check if it is a Windows path string.
331
   * @return {@code true} if it is a valid windows path string, else {@code false}.
332
   */
333
  public static boolean isValidWindowsPath(String pathString) {
334

335
    return REGEX_WINDOWS_PATH.matcher(pathString).matches();
5✔
336
  }
337
}
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

© 2025 Coveralls, Inc