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

devonfw / IDEasy / 19436245827

17 Nov 2025 04:09PM UTC coverage: 68.942% (-0.004%) from 68.946%
19436245827

Pull #1592

github

web-flow
Merge 2b6d0804e into 70a90c5e4
Pull Request #1592: #1590: Add SystemPath.findBinary(Path toolPath, Predicate<Path> filter)

3527 of 5605 branches covered (62.93%)

Branch coverage included in aggregate %.

9210 of 12870 relevant lines covered (71.56%)

3.14 hits per line

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

86.19
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 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, pathSeparator, extraPathEntries, new HashMap<>(), new ArrayList<>());
11✔
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
  /**
109
   * @param context {@link IdeContext} for the output of information.
110
   * @param softwarePath the {@link IdeContext#getSoftwarePath() software path}.
111
   * @param pathSeparator the path separator char (';' for Windows and ':' otherwise).
112
   * @param paths the {@link List} of {@link Path}s to use in the PATH environment variable.
113
   */
114
  public SystemPath(IdeContext context, Path softwarePath, char pathSeparator, List<Path> paths) {
115
    this(context, pathSeparator, new ArrayList<>(), new HashMap<>(), paths);
11✔
116
    collectToolPath(softwarePath);
3✔
117
  }
1✔
118

119
  private SystemPath(IdeContext context, char pathSeparator, List<Path> extraPathEntries, Map<String, Path> tool2PathMap, List<Path> paths) {
120

121
    super();
2✔
122
    this.context = context;
3✔
123
    this.pathSeparator = pathSeparator;
3✔
124
    this.extraPathEntries = extraPathEntries;
3✔
125
    this.tool2pathMap = tool2PathMap;
3✔
126
    this.paths = paths;
3✔
127
  }
1✔
128

129
  private void collectToolPath(Path softwarePath) {
130

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

155
  private static String getTool(Path path, Path ideRoot) {
156

157
    if (ideRoot == null) {
2✔
158
      return null;
2✔
159
    }
160
    if (path.startsWith(ideRoot)) {
4✔
161
      Path relativized = ideRoot.relativize(path);
4✔
162
      int count = relativized.getNameCount();
3✔
163
      if (count >= 3) {
3!
164
        if (relativized.getName(1).toString().equals("software")) {
×
165
          return relativized.getName(2).toString();
×
166
        }
167
      }
168
    }
169
    return null;
2✔
170
  }
171

172
  private Path findBinaryInOrder(Path path, String tool) {
173

174
    List<String> extensionPriority = List.of("");
3✔
175
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
176
      extensionPriority = EXTENSION_PRIORITY;
×
177
    }
178
    for (String extension : extensionPriority) {
10✔
179

180
      Path fileToExecute = path.resolve(tool + extension);
6✔
181

182
      if (Files.exists(fileToExecute, LinkOption.NOFOLLOW_LINKS)) {
9✔
183
        return fileToExecute;
2✔
184
      }
185
    }
1✔
186

187
    return null;
2✔
188
  }
189

190
  /**
191
   * @param binaryName the name of the tool.
192
   * @return {@code true} if the given {@code tool} is a binary that can be found on the PATH, {@code false} otherwise.
193
   */
194
  public boolean hasBinaryOnPath(String binaryName) {
195
    Path binary = Path.of(binaryName);
×
196
    Path resolvedBinary = findBinary(binary);
×
197
    return (resolvedBinary != binary);
×
198
  }
199

200
  /**
201
   * @param binaryName the name of the tool.
202
   * @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
203
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
204
   */
205
  public Path findBinaryPathByName(String binaryName) {
206
    return findBinary(Path.of(binaryName));
×
207
  }
208

209
  /**
210
   * @param toolPath the {@link Path} to the tool installation.
211
   * @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
212
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
213
   */
214
  public Path findBinary(Path toolPath) {
215
    return findBinary(toolPath, p -> true);
7✔
216
  }
217

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

230
    Path parent = toolPath.getParent();
3✔
231
    String fileName = toolPath.getFileName().toString();
4✔
232

233
    if (parent == null) {
2✔
234
      for (Path path : this.tool2pathMap.values()) {
12✔
235
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
236
        if (binaryPath != null && filter.test(binaryPath)) {
6✔
237
          return binaryPath;
2✔
238
        }
239
      }
1✔
240
      for (Path path : this.paths) {
11✔
241
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
242
        if (binaryPath != null && filter.test(binaryPath)) {
6!
243
          return binaryPath;
2✔
244
        }
245
      }
2✔
246
    } else {
247
      Path binaryPath = findBinaryInOrder(parent, fileName);
5✔
248
      if (binaryPath != null && filter.test(binaryPath)) {
6!
249
        return binaryPath;
2✔
250
      }
251
    }
252

253
    return toolPath;
2✔
254
  }
255

256
  /**
257
   * @param tool the name of the tool.
258
   * @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.
259
   */
260
  public Path getPath(String tool) {
261

262
    return this.tool2pathMap.get(tool);
6✔
263
  }
264

265
  /**
266
   * @param tool the name of the tool.
267
   * @param path the new {@link #getPath(String) tool bin path}.
268
   */
269
  public void setPath(String tool, Path path) {
270

271
    this.tool2pathMap.put(tool, path);
6✔
272
  }
1✔
273

274
  @Override
275
  public String toString() {
276

277
    return toString(null);
4✔
278
  }
279

280
  /**
281
   * @param pathSyntax the {@link WindowsPathSyntax} to convert to.
282
   * @return this {@link SystemPath} as {@link String} for the PATH environment variable.
283
   */
284
  public String toString(WindowsPathSyntax pathSyntax) {
285

286
    char separator;
287
    if (pathSyntax == WindowsPathSyntax.MSYS) {
3!
288
      separator = ':';
×
289
    } else {
290
      separator = this.pathSeparator;
3✔
291
    }
292
    StringBuilder sb = new StringBuilder(128);
5✔
293
    for (Path path : this.extraPathEntries) {
11✔
294
      appendPath(path, sb, separator, pathSyntax);
5✔
295
    }
1✔
296
    for (Path path : this.tool2pathMap.values()) {
12✔
297
      appendPath(path, sb, separator, pathSyntax);
5✔
298
    }
1✔
299
    for (Path path : this.paths) {
11✔
300
      appendPath(path, sb, separator, pathSyntax);
5✔
301
    }
1✔
302
    return sb.toString();
3✔
303
  }
304

305
  /**
306
   * Derive a new {@link SystemPath} from this instance with the given parameters.
307
   *
308
   * @param overriddenPath the entire PATH to override and replace the current one from this {@link SystemPath} or {@code null} to keep the current PATH.
309
   * @param extraPathEntries the {@link List} of additional PATH entries to add to the beginning of the PATH. May be empty to add nothing.
310
   * @return the new {@link SystemPath} derived from this instance with the given parameters applied.
311
   */
312
  public SystemPath withPath(String overriddenPath, List<Path> extraPathEntries) {
313

314
    if (overriddenPath == null) {
2!
315
      return new SystemPath(this.context, this.pathSeparator, extraPathEntries, this.tool2pathMap, this.paths);
13✔
316
    } else {
317
      return new SystemPath(this.context, overriddenPath, null, null, this.pathSeparator, extraPathEntries);
×
318
    }
319
  }
320

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

323
    if (sb.length() > 0) {
3✔
324
      sb.append(separator);
4✔
325
    }
326
    String pathString;
327
    if (pathSyntax == null) {
2✔
328
      pathString = path.toString();
4✔
329
    } else {
330
      pathString = pathSyntax.format(path);
4✔
331
    }
332
    sb.append(pathString);
4✔
333
  }
1✔
334

335
  /**
336
   * Method to validate if a given path string is a Windows path or not
337
   *
338
   * @param pathString The string to check if it is a Windows path string.
339
   * @return {@code true} if it is a valid windows path string, else {@code false}.
340
   */
341
  public static boolean isValidWindowsPath(String pathString) {
342

343
    return REGEX_WINDOWS_PATH.matcher(pathString).matches();
5✔
344
  }
345
}
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