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

devonfw / IDEasy / 22241505980

20 Feb 2026 09:16PM UTC coverage: 70.656% (+0.2%) from 70.474%
22241505980

Pull #1710

github

web-flow
Merge 04e4bdacd into 379acdc9d
Pull Request #1710: #404: allow logging via SLF4J

4121 of 6440 branches covered (63.99%)

Branch coverage included in aggregate %.

10704 of 14542 relevant lines covered (73.61%)

3.13 hits per line

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

66.88
cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
1
package com.devonfw.tools.ide.context;
2

3
import static com.devonfw.tools.ide.variable.IdeVariables.IDE_MIN_VERSION;
4

5
import java.io.ByteArrayInputStream;
6
import java.io.ByteArrayOutputStream;
7
import java.io.IOException;
8
import java.nio.file.Files;
9
import java.nio.file.Path;
10
import java.time.LocalDateTime;
11
import java.util.ArrayList;
12
import java.util.HashMap;
13
import java.util.Iterator;
14
import java.util.List;
15
import java.util.Locale;
16
import java.util.Map;
17
import java.util.Map.Entry;
18
import java.util.Objects;
19
import java.util.Properties;
20
import java.util.function.Predicate;
21
import java.util.logging.LogManager;
22

23
import org.slf4j.Logger;
24
import org.slf4j.LoggerFactory;
25

26
import com.devonfw.tools.ide.cli.CliAbortException;
27
import com.devonfw.tools.ide.cli.CliArgument;
28
import com.devonfw.tools.ide.cli.CliArguments;
29
import com.devonfw.tools.ide.cli.CliException;
30
import com.devonfw.tools.ide.commandlet.Commandlet;
31
import com.devonfw.tools.ide.commandlet.CommandletManager;
32
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
33
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
34
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
35
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
36
import com.devonfw.tools.ide.common.SystemPath;
37
import com.devonfw.tools.ide.completion.CompletionCandidate;
38
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
39
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
40
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
41
import com.devonfw.tools.ide.environment.EnvironmentVariables;
42
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
43
import com.devonfw.tools.ide.environment.IdeSystem;
44
import com.devonfw.tools.ide.environment.IdeSystemImpl;
45
import com.devonfw.tools.ide.git.GitContext;
46
import com.devonfw.tools.ide.git.GitContextImpl;
47
import com.devonfw.tools.ide.git.GitUrl;
48
import com.devonfw.tools.ide.io.FileAccess;
49
import com.devonfw.tools.ide.io.FileAccessImpl;
50
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
51
import com.devonfw.tools.ide.log.IdeLogLevel;
52
import com.devonfw.tools.ide.log.IdeLogger;
53
import com.devonfw.tools.ide.log.IdeSubLogger;
54
import com.devonfw.tools.ide.log.IdeSubLoggerAdapter;
55
import com.devonfw.tools.ide.merge.DirectoryMerger;
56
import com.devonfw.tools.ide.migration.IdeMigrator;
57
import com.devonfw.tools.ide.network.NetworkStatus;
58
import com.devonfw.tools.ide.network.NetworkStatusImpl;
59
import com.devonfw.tools.ide.os.SystemInfo;
60
import com.devonfw.tools.ide.os.SystemInfoImpl;
61
import com.devonfw.tools.ide.os.WindowsHelper;
62
import com.devonfw.tools.ide.os.WindowsHelperImpl;
63
import com.devonfw.tools.ide.os.WindowsPathSyntax;
64
import com.devonfw.tools.ide.process.ProcessContext;
65
import com.devonfw.tools.ide.process.ProcessContextImpl;
66
import com.devonfw.tools.ide.process.ProcessResult;
67
import com.devonfw.tools.ide.property.Property;
68
import com.devonfw.tools.ide.step.Step;
69
import com.devonfw.tools.ide.step.StepImpl;
70
import com.devonfw.tools.ide.tool.custom.CustomToolRepository;
71
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
72
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
73
import com.devonfw.tools.ide.tool.npm.NpmRepository;
74
import com.devonfw.tools.ide.tool.pip.PipRepository;
75
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
76
import com.devonfw.tools.ide.tool.repository.ToolRepository;
77
import com.devonfw.tools.ide.url.model.UrlMetadata;
78
import com.devonfw.tools.ide.util.DateTimeUtil;
79
import com.devonfw.tools.ide.util.PrivacyUtil;
80
import com.devonfw.tools.ide.validation.ValidationResult;
81
import com.devonfw.tools.ide.validation.ValidationResultValid;
82
import com.devonfw.tools.ide.validation.ValidationState;
83
import com.devonfw.tools.ide.variable.IdeVariables;
84
import com.devonfw.tools.ide.version.IdeVersion;
85
import com.devonfw.tools.ide.version.VersionIdentifier;
86

87
/**
88
 * Abstract base implementation of {@link IdeContext}.
89
 */
90
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
91

92
  private static final Logger LOG = LoggerFactory.getLogger(AbstractIdeContext.class);
3✔
93

94
  /** The default shell bash (Bourne Again SHell). */
95
  public static final String BASH = "bash";
96

97
  private static final GitUrl IDE_URLS_GIT = new GitUrl("https://github.com/devonfw/ide-urls.git", null);
7✔
98

99
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
100

101
  private static final String DEFAULT_WINDOWS_GIT_PATH = "C:\\Program Files\\Git\\bin\\bash.exe";
102

103
  private static final String OPTION_DETAILS_START = "([";
104

105
  private final IdeStartContextImpl startContext;
106

107
  private Path ideHome;
108

109
  private final Path ideRoot;
110

111
  private Path confPath;
112

113
  protected Path settingsPath;
114

115
  private Path settingsCommitIdPath;
116

117
  protected Path pluginsPath;
118

119
  private Path workspacePath;
120

121
  private String workspaceName;
122

123
  private Path cwd;
124

125
  private Path downloadPath;
126

127
  private Path userHome;
128

129
  private Path userHomeIde;
130

131
  private SystemPath path;
132

133
  private WindowsPathSyntax pathSyntax;
134

135
  private final SystemInfo systemInfo;
136

137
  private EnvironmentVariables variables;
138

139
  private final FileAccess fileAccess;
140

141
  protected CommandletManager commandletManager;
142

143
  protected ToolRepository defaultToolRepository;
144

145
  private CustomToolRepository customToolRepository;
146

147
  private MvnRepository mvnRepository;
148

149
  private NpmRepository npmRepository;
150

151
  private PipRepository pipRepository;
152

153
  private DirectoryMerger workspaceMerger;
154

155
  protected UrlMetadata urlMetadata;
156

157
  protected Path defaultExecutionDirectory;
158

159
  private StepImpl currentStep;
160

161
  private NetworkStatus networkStatus;
162

163
  protected IdeSystem system;
164

165
  private WindowsHelper windowsHelper;
166

167
  private final Map<String, String> privacyMap;
168

169
  private Path bash;
170

171
  private boolean julConfigured;
172

173
  private final IdeSubLogger[] loggers;
174

175
  /**
176
   * The constructor.
177
   *
178
   * @param startContext the {@link IdeLogger}.
179
   * @param workingDirectory the optional {@link Path} to current working directory.
180
   */
181
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
182

183
    super();
2✔
184
    // init sub loggers
185
    IdeLogLevel[] levels = IdeLogLevel.values();
2✔
186
    this.loggers = new IdeSubLogger[levels.length];
5✔
187
    for (IdeLogLevel level : levels) {
16✔
188
      this.loggers[level.ordinal()] = new IdeSubLoggerAdapter(level, LOG);
10✔
189
    }
190

191
    this.startContext = startContext;
3✔
192
    this.startContext.setArgFormatter(this);
4✔
193
    this.privacyMap = new HashMap<>();
5✔
194
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
195
    this.commandletManager = new CommandletManagerImpl(this);
6✔
196
    this.fileAccess = new FileAccessImpl(this);
6✔
197
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
198
    if (userHomeProperty != null) {
2!
199
      this.userHome = Path.of(userHomeProperty);
×
200
    }
201
    if (workingDirectory == null) {
2!
202
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
203
    }
204
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
205
    if (Files.isDirectory(workingDirectory)) {
5✔
206
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
207
    } else {
208
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
209
    }
210
    this.cwd = workingDirectory;
3✔
211
    // detect IDE_HOME and WORKSPACE
212
    String workspace = null;
2✔
213
    Path ideHomeDir = null;
2✔
214
    IdeHomeAndWorkspace ideHomeAndWorkspace = findIdeHome(workingDirectory);
4✔
215
    if (ideHomeAndWorkspace != null) {
2!
216
      ideHomeDir = ideHomeAndWorkspace.home();
3✔
217
      workspace = ideHomeAndWorkspace.workspace();
3✔
218
    }
219

220
    // detection completed, initializing variables
221
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
222

223
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
224

225
    if (this.ideRoot != null) {
3✔
226
      Path tempDownloadPath = getTempDownloadPath();
3✔
227
      if (Files.isDirectory(tempDownloadPath)) {
6✔
228
        // TODO delete all files older than 1 day here...
229
      } else {
230
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
231
      }
232
    }
233
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
234
  }
1✔
235

236
  /**
237
   * Searches for the IDE home directory by traversing up the directory tree from the given working directory. This method can be overridden in test contexts to
238
   * add additional validation or boundary checks.
239
   *
240
   * @param workingDirectory the starting directory for the search.
241
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
242
   */
243
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
244

245
    Path currentDir = workingDirectory;
2✔
246
    String name1 = "";
2✔
247
    String name2 = "";
2✔
248
    String workspace = WORKSPACE_MAIN;
2✔
249
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
250

251
    while (currentDir != null) {
2✔
252
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
253
      if (isIdeHome(currentDir)) {
4✔
254
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
255
          workspace = name2;
3✔
256
        }
257
        break;
258
      }
259
      name2 = name1;
2✔
260
      int nameCount = currentDir.getNameCount();
3✔
261
      if (nameCount >= 1) {
3✔
262
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
263
      }
264
      currentDir = currentDir.getParent();
3✔
265
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
266
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
267
        currentDir = null;
×
268
      }
269
    }
1✔
270

271
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
272
  }
273

274
  /**
275
   * @return a new {@link MvnRepository}
276
   */
277
  protected MvnRepository createMvnRepository() {
278
    return new MvnRepository(this);
5✔
279
  }
280

281
  /**
282
   * @return a new {@link NpmRepository}
283
   */
284
  protected NpmRepository createNpmRepository() {
285
    return new NpmRepository(this);
×
286
  }
287

288
  /**
289
   * @return a new {@link PipRepository}
290
   */
291
  protected PipRepository createPipRepository() {
292
    return new PipRepository(this);
×
293
  }
294

295
  private Path findIdeRoot(Path ideHomePath) {
296

297
    Path ideRootPath = null;
2✔
298
    if (ideHomePath != null) {
2✔
299
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
300
      ideRootPath = ideHomePath.getParent();
3✔
301
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
302
        warning(
×
303
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
304
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
305
            ideRootPathFromEnv,
306
            ideHomePath.getFileName(), ideRootPath);
×
307
      }
308
    } else if (!isTest()) {
4!
309
      ideRootPath = getIdeRootPathFromEnv(true);
×
310
    }
311
    return ideRootPath;
2✔
312
  }
313

314
  /**
315
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
316
   */
317
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
318

319
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
320
    if (root != null) {
×
321
      Path rootPath = Path.of(root);
×
322
      if (Files.isDirectory(rootPath)) {
×
323
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
324
        if (withSanityCheck) {
×
325
          int nameCount = rootPath.getNameCount();
×
326
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
327
          int delta = absoluteNameCount - nameCount;
×
328
          if (delta >= 0) {
×
329
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
330
              String rootName = rootPath.getName(nameIndex).toString();
×
331
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
332
              if (!rootName.equals(absoluteRootName)) {
×
333
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
334
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
335
                break;
×
336
              }
337
            }
338
          } else {
339
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
340
                absoluteRootPath);
341
          }
342
        }
343
        return absoluteRootPath;
×
344
      } else if (withSanityCheck) {
×
345
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
346
      }
347
    }
348
    return null;
×
349
  }
350

351
  @Override
352
  public void setCwd(Path userDir, String workspace, Path ideHome) {
353

354
    this.cwd = userDir;
3✔
355
    this.workspaceName = workspace;
3✔
356
    this.ideHome = ideHome;
3✔
357
    if (ideHome == null) {
2✔
358
      this.workspacePath = null;
3✔
359
      this.confPath = null;
3✔
360
      this.settingsPath = null;
3✔
361
      this.pluginsPath = null;
4✔
362
    } else {
363
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
364
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
365
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
366
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
367
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
368
    }
369
    if (isTest()) {
3!
370
      // only for testing...
371
      if (this.ideHome == null) {
3✔
372
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
373
      } else {
374
        this.userHome = this.ideHome.resolve("home");
6✔
375
      }
376
    }
377
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
378
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
379
    resetPrivacyMap();
2✔
380
    this.path = computeSystemPath();
4✔
381
  }
1✔
382

383
  private String getMessageIdeHomeFound() {
384

385
    String wks = this.workspaceName;
3✔
386
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
387
      wks = "*".repeat(wks.length());
×
388
    }
389
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
390
  }
391

392
  private String getMessageNotInsideIdeProject() {
393

394
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
395
  }
396

397
  private String getMessageIdeRootNotFound() {
398

399
    String root = getSystem().getEnv("IDE_ROOT");
5✔
400
    if (root == null) {
2!
401
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
402
    } else {
403
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
404
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
405
    }
406
  }
407

408
  /**
409
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
410
   */
411
  public boolean isTest() {
412

413
    return false;
×
414
  }
415

416
  protected SystemPath computeSystemPath() {
417

418
    return new SystemPath(this);
×
419
  }
420

421
  /**
422
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
423
   *
424
   * @param dir the directory to check.
425
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
426
   */
427
  protected boolean isIdeHome(Path dir) {
428

429
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
430
      return false;
2✔
431
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
432
      return false;
×
433
    }
434
    return true;
2✔
435
  }
436

437
  private EnvironmentVariables createVariables() {
438

439
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
440
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
441
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
442
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
443
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
444
    return conf.resolved();
3✔
445
  }
446

447
  protected AbstractEnvironmentVariables createSystemVariables() {
448

449
    return EnvironmentVariables.ofSystem(this);
3✔
450
  }
451

452
  @Override
453
  public SystemInfo getSystemInfo() {
454

455
    return this.systemInfo;
3✔
456
  }
457

458
  @Override
459
  public FileAccess getFileAccess() {
460

461
    return this.fileAccess;
3✔
462
  }
463

464
  @Override
465
  public CommandletManager getCommandletManager() {
466

467
    return this.commandletManager;
3✔
468
  }
469

470
  @Override
471
  public ToolRepository getDefaultToolRepository() {
472

473
    return this.defaultToolRepository;
3✔
474
  }
475

476
  @Override
477
  public MvnRepository getMvnRepository() {
478
    if (this.mvnRepository == null) {
3✔
479
      this.mvnRepository = createMvnRepository();
4✔
480
    }
481
    return this.mvnRepository;
3✔
482
  }
483

484
  @Override
485
  public NpmRepository getNpmRepository() {
486
    if (this.npmRepository == null) {
3✔
487
      this.npmRepository = createNpmRepository();
4✔
488
    }
489
    return this.npmRepository;
3✔
490
  }
491

492
  @Override
493
  public PipRepository getPipRepository() {
494
    if (this.pipRepository == null) {
3✔
495
      this.pipRepository = createPipRepository();
4✔
496
    }
497
    return this.pipRepository;
3✔
498
  }
499

500
  @Override
501
  public CustomToolRepository getCustomToolRepository() {
502

503
    if (this.customToolRepository == null) {
3!
504
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
505
    }
506
    return this.customToolRepository;
3✔
507
  }
508

509
  @Override
510
  public Path getIdeHome() {
511

512
    return this.ideHome;
3✔
513
  }
514

515
  @Override
516
  public String getProjectName() {
517

518
    if (this.ideHome != null) {
3!
519
      return this.ideHome.getFileName().toString();
5✔
520
    }
521
    return "";
×
522
  }
523

524
  @Override
525
  public VersionIdentifier getProjectVersion() {
526

527
    if (this.ideHome != null) {
3!
528
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
529
      if (Files.exists(versionFile)) {
5✔
530
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
531
        return VersionIdentifier.of(version);
3✔
532
      }
533
    }
534
    return IdeMigrator.START_VERSION;
2✔
535
  }
536

537
  @Override
538
  public void setProjectVersion(VersionIdentifier version) {
539

540
    if (this.ideHome == null) {
3!
541
      throw new IllegalStateException("IDE_HOME not available!");
×
542
    }
543
    Objects.requireNonNull(version);
3✔
544
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
545
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
546
  }
1✔
547

548
  @Override
549
  public Path getIdeRoot() {
550

551
    return this.ideRoot;
3✔
552
  }
553

554
  @Override
555
  public Path getIdePath() {
556

557
    Path myIdeRoot = getIdeRoot();
3✔
558
    if (myIdeRoot == null) {
2✔
559
      return null;
2✔
560
    }
561
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
562
  }
563

564
  @Override
565
  public Path getCwd() {
566

567
    return this.cwd;
3✔
568
  }
569

570
  @Override
571
  public Path getTempPath() {
572

573
    Path idePath = getIdePath();
3✔
574
    if (idePath == null) {
2!
575
      return null;
×
576
    }
577
    return idePath.resolve("tmp");
4✔
578
  }
579

580
  @Override
581
  public Path getTempDownloadPath() {
582

583
    Path tmp = getTempPath();
3✔
584
    if (tmp == null) {
2!
585
      return null;
×
586
    }
587
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
588
  }
589

590
  @Override
591
  public Path getUserHome() {
592

593
    return this.userHome;
3✔
594
  }
595

596
  /**
597
   * This method should only be used for tests to mock user home.
598
   *
599
   * @param userHome the new value of {@link #getUserHome()}.
600
   */
601
  protected void setUserHome(Path userHome) {
602

603
    this.userHome = userHome;
3✔
604
    resetPrivacyMap();
2✔
605
  }
1✔
606

607
  @Override
608
  public Path getUserHomeIde() {
609

610
    return this.userHomeIde;
3✔
611
  }
612

613
  @Override
614
  public Path getSettingsPath() {
615

616
    return this.settingsPath;
3✔
617
  }
618

619
  @Override
620
  public Path getSettingsGitRepository() {
621

622
    Path settingsPath = getSettingsPath();
3✔
623
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
624
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
625
      error("Settings repository exists but is not a git repository.");
3✔
626
      return null;
2✔
627
    }
628
    return settingsPath;
2✔
629
  }
630

631
  @Override
632
  public boolean isSettingsRepositorySymlinkOrJunction() {
633

634
    Path settingsPath = getSettingsPath();
3✔
635
    if (settingsPath == null) {
2!
636
      return false;
×
637
    }
638
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
639
  }
640

641
  @Override
642
  public Path getSettingsCommitIdPath() {
643

644
    return this.settingsCommitIdPath;
3✔
645
  }
646

647
  @Override
648
  public Path getConfPath() {
649

650
    return this.confPath;
3✔
651
  }
652

653
  @Override
654
  public Path getSoftwarePath() {
655

656
    if (this.ideHome == null) {
3✔
657
      return null;
2✔
658
    }
659
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
660
  }
661

662
  @Override
663
  public Path getSoftwareExtraPath() {
664

665
    Path softwarePath = getSoftwarePath();
3✔
666
    if (softwarePath == null) {
2!
667
      return null;
×
668
    }
669
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
670
  }
671

672
  @Override
673
  public Path getSoftwareRepositoryPath() {
674

675
    Path idePath = getIdePath();
3✔
676
    if (idePath == null) {
2!
677
      return null;
×
678
    }
679
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
680
  }
681

682
  @Override
683
  public Path getPluginsPath() {
684

685
    return this.pluginsPath;
3✔
686
  }
687

688
  @Override
689
  public String getWorkspaceName() {
690

691
    return this.workspaceName;
3✔
692
  }
693

694
  @Override
695
  public Path getWorkspacePath() {
696

697
    return this.workspacePath;
3✔
698
  }
699

700
  @Override
701
  public Path getDownloadPath() {
702

703
    return this.downloadPath;
3✔
704
  }
705

706
  @Override
707
  public Path getUrlsPath() {
708

709
    Path idePath = getIdePath();
3✔
710
    if (idePath == null) {
2!
711
      return null;
×
712
    }
713
    return idePath.resolve(FOLDER_URLS);
4✔
714
  }
715

716
  @Override
717
  public Path getToolRepositoryPath() {
718

719
    Path idePath = getIdePath();
3✔
720
    if (idePath == null) {
2!
721
      return null;
×
722
    }
723
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
724
  }
725

726
  @Override
727
  public SystemPath getPath() {
728

729
    return this.path;
3✔
730
  }
731

732
  @Override
733
  public EnvironmentVariables getVariables() {
734

735
    if (this.variables == null) {
3✔
736
      this.variables = createVariables();
4✔
737
    }
738
    return this.variables;
3✔
739
  }
740

741
  @Override
742
  public UrlMetadata getUrls() {
743

744
    if (this.urlMetadata == null) {
3✔
745
      if (!isTest()) {
3!
746
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
747
      }
748
      this.urlMetadata = new UrlMetadata(this);
6✔
749
    }
750
    return this.urlMetadata;
3✔
751
  }
752

753
  @Override
754
  public boolean isQuietMode() {
755

756
    return this.startContext.isQuietMode();
4✔
757
  }
758

759
  @Override
760
  public boolean isBatchMode() {
761

762
    return this.startContext.isBatchMode();
4✔
763
  }
764

765
  @Override
766
  public boolean isForceMode() {
767

768
    return this.startContext.isForceMode();
4✔
769
  }
770

771
  @Override
772
  public boolean isForcePull() {
773

774
    return this.startContext.isForcePull();
4✔
775
  }
776

777
  @Override
778
  public boolean isForcePlugins() {
779

780
    return this.startContext.isForcePlugins();
4✔
781
  }
782

783
  @Override
784
  public boolean isForceRepositories() {
785

786
    return this.startContext.isForceRepositories();
4✔
787
  }
788

789
  @Override
790
  public boolean isOfflineMode() {
791

792
    return this.startContext.isOfflineMode();
4✔
793
  }
794

795
  @Override
796
  public boolean isPrivacyMode() {
797
    return this.startContext.isPrivacyMode();
4✔
798
  }
799

800
  @Override
801
  public boolean isSkipUpdatesMode() {
802

803
    return this.startContext.isSkipUpdatesMode();
4✔
804
  }
805

806
  @Override
807
  public boolean isNoColorsMode() {
808

809
    return this.startContext.isNoColorsMode();
×
810
  }
811

812
  @Override
813
  public NetworkStatus getNetworkStatus() {
814

815
    if (this.networkStatus == null) {
×
816
      this.networkStatus = new NetworkStatusImpl(this);
×
817
    }
818
    return this.networkStatus;
×
819
  }
820

821
  @Override
822
  public Locale getLocale() {
823

824
    Locale locale = this.startContext.getLocale();
4✔
825
    if (locale == null) {
2✔
826
      locale = Locale.getDefault();
2✔
827
    }
828
    return locale;
2✔
829
  }
830

831
  @Override
832
  public DirectoryMerger getWorkspaceMerger() {
833

834
    if (this.workspaceMerger == null) {
3✔
835
      this.workspaceMerger = new DirectoryMerger(this);
6✔
836
    }
837
    return this.workspaceMerger;
3✔
838
  }
839

840
  /**
841
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
842
   */
843
  @Override
844
  public Path getDefaultExecutionDirectory() {
845

846
    return this.defaultExecutionDirectory;
×
847
  }
848

849
  /**
850
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
851
   */
852
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
853

854
    if (defaultExecutionDirectory != null) {
×
855
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
856
    }
857
  }
×
858

859
  @Override
860
  public GitContext getGitContext() {
861

862
    return new GitContextImpl(this);
×
863
  }
864

865
  @Override
866
  public ProcessContext newProcess() {
867

868
    ProcessContext processContext = createProcessContext();
3✔
869
    if (this.defaultExecutionDirectory != null) {
3!
870
      processContext.directory(this.defaultExecutionDirectory);
×
871
    }
872
    return processContext;
2✔
873
  }
874

875
  @Override
876
  public IdeSystem getSystem() {
877

878
    if (this.system == null) {
×
879
      this.system = new IdeSystemImpl();
×
880
    }
881
    return this.system;
×
882
  }
883

884
  /**
885
   * @return a new instance of {@link ProcessContext}.
886
   * @see #newProcess()
887
   */
888
  protected ProcessContext createProcessContext() {
889

890
    return new ProcessContextImpl(this);
×
891
  }
892

893
  @Override
894
  public IdeSubLogger level(IdeLogLevel level) {
895

896
    return this.loggers[level.ordinal()];
6✔
897
  }
898

899
  @Override
900
  public void logIdeHomeAndRootStatus() {
901
    if (this.ideRoot != null) {
3!
902
      success("IDE_ROOT is set to {}", this.ideRoot);
×
903
    }
904
    if (this.ideHome == null) {
3✔
905
      warning(getMessageNotInsideIdeProject());
5✔
906
    } else {
907
      success("IDE_HOME is set to {}", this.ideHome);
10✔
908
    }
909
  }
1✔
910

911
  @Override
912
  public String formatArgument(Object argument) {
913

914
    if (argument == null) {
2✔
915
      return null;
2✔
916
    }
917
    String result = argument.toString();
3✔
918
    if (isPrivacyMode()) {
3✔
919
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
920
        initializePrivacyMap(this.userHome, "~");
×
921
        String projectName = getProjectName();
×
922
        if (!projectName.isEmpty()) {
×
923
          this.privacyMap.put(projectName, "project");
×
924
        }
925
      }
926
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
927
        result = result.replace(entry.getKey(), entry.getValue());
×
928
      }
×
929
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
930
    }
931
    return result;
2✔
932
  }
933

934
  /**
935
   * @param path the sensitive {@link Path} to
936
   * @param replacement the replacement to mask the {@link Path} in log output.
937
   */
938
  protected void initializePrivacyMap(Path path, String replacement) {
939

940
    if (path == null) {
×
941
      return;
×
942
    }
943
    if (this.systemInfo.isWindows()) {
×
944
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
945
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
946
    } else {
947
      this.privacyMap.put(path.toString(), replacement);
×
948
    }
949
  }
×
950

951
  /**
952
   * Resets the privacy map in case fundamental values have changed.
953
   */
954
  private void resetPrivacyMap() {
955

956
    this.privacyMap.clear();
3✔
957
  }
1✔
958

959

960
  @Override
961
  public String askForInput(String message, String defaultValue) {
962

963
    while (true) {
964
      if (!message.isBlank()) {
3!
965
        interaction(message);
3✔
966
      }
967
      if (isBatchMode()) {
3!
968
        if (isForceMode()) {
×
969
          return defaultValue;
×
970
        } else {
971
          throw new CliAbortException();
×
972
        }
973
      }
974
      String input = readLine().trim();
4✔
975
      if (!input.isEmpty()) {
3!
976
        return input;
2✔
977
      } else {
978
        if (defaultValue != null) {
×
979
          return defaultValue;
×
980
        }
981
      }
982
    }
×
983
  }
984

985
  @SuppressWarnings("unchecked")
986
  @Override
987
  public <O> O question(O[] options, String question, Object... args) {
988

989
    assert (options.length > 0);
4!
990
    interaction(question, args);
4✔
991
    return displayOptionsAndGetAnswer(options);
4✔
992
  }
993

994
  private <O> O displayOptionsAndGetAnswer(O[] options) {
995
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
996
    int i = 0;
2✔
997
    for (O option : options) {
16✔
998
      i++;
1✔
999
      String title = "" + option;
4✔
1000
      String key = computeOptionKey(title);
3✔
1001
      addMapping(mapping, key, option);
4✔
1002
      String numericKey = Integer.toString(i);
3✔
1003
      if (numericKey.equals(key)) {
4!
1004
        trace("Options should not be numeric: " + key);
×
1005
      } else {
1006
        addMapping(mapping, numericKey, option);
4✔
1007
      }
1008
      interaction("Option " + numericKey + ": " + title);
5✔
1009
    }
1010
    O option = null;
2✔
1011
    if (isBatchMode()) {
3!
1012
      if (isForceMode()) {
×
1013
        option = options[0];
×
1014
        interaction("" + option);
×
1015
      }
1016
    } else {
1017
      while (option == null) {
2✔
1018
        String answer = readLine();
3✔
1019
        option = mapping.get(answer);
4✔
1020
        if (option == null) {
2!
1021
          warning("Invalid answer: '" + answer + "' - please try again.");
×
1022
        }
1023
      }
1✔
1024
    }
1025
    return option;
2✔
1026
  }
1027

1028
  private static String computeOptionKey(String option) {
1029
    String key = option;
2✔
1030
    int index = -1;
2✔
1031
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1032
      int currentIndex = key.indexOf(c);
4✔
1033
      if (currentIndex != -1) {
3✔
1034
        if ((index == -1) || (currentIndex < index)) {
3!
1035
          index = currentIndex;
2✔
1036
        }
1037
      }
1038
    }
1039
    if (index > 0) {
2✔
1040
      key = key.substring(0, index).trim();
6✔
1041
    }
1042
    return key;
2✔
1043
  }
1044

1045
  /**
1046
   * @return the input from the end-user (e.g. read from the console).
1047
   */
1048
  protected abstract String readLine();
1049

1050
  private static <O> void addMapping(Map<String, O> mapping, String key, O option) {
1051

1052
    O duplicate = mapping.put(key, option);
5✔
1053
    if (duplicate != null) {
2!
1054
      throw new IllegalArgumentException("Duplicated option " + key);
×
1055
    }
1056
  }
1✔
1057

1058
  @Override
1059
  public Step getCurrentStep() {
1060

1061
    return this.currentStep;
×
1062
  }
1063

1064
  @Override
1065
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1066

1067
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1068
    return this.currentStep;
3✔
1069
  }
1070

1071
  /**
1072
   * Internal method to end the running {@link Step}.
1073
   *
1074
   * @param step the current {@link Step} to end.
1075
   */
1076
  public void endStep(StepImpl step) {
1077

1078
    if (step == this.currentStep) {
4!
1079
      this.currentStep = this.currentStep.getParent();
6✔
1080
    } else {
1081
      String currentStepName = "null";
×
1082
      if (this.currentStep != null) {
×
1083
        currentStepName = this.currentStep.getName();
×
1084
      }
1085
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1086
    }
1087
  }
1✔
1088

1089
  /**
1090
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1091
   *
1092
   * @param arguments the {@link CliArgument}.
1093
   * @return the return code of the execution.
1094
   */
1095
  public int run(CliArguments arguments) {
1096

1097
    CliArgument current = arguments.current();
3✔
1098
    assert (this.currentStep == null);
4!
1099
    boolean supressStepSuccess = false;
2✔
1100
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1101
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1102
    Commandlet cmd = null;
2✔
1103
    ValidationResult result = null;
2✔
1104
    try {
1105
      while (commandletIterator.hasNext()) {
3✔
1106
        cmd = commandletIterator.next();
4✔
1107
        result = applyAndRun(arguments.copy(), cmd);
6✔
1108
        if (result.isValid()) {
3!
1109
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1110
          step.success();
2✔
1111
          return ProcessResult.SUCCESS;
4✔
1112
        }
1113
      }
1114
      this.startContext.activateLogging();
3✔
1115
      verifyIdeMinVersion(false);
3✔
1116
      if (result != null) {
2!
1117
        error(result.getErrorMessage());
×
1118
      }
1119
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1120
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1121
      if (cmd != null) {
2!
1122
        help.commandlet.setValue(cmd);
×
1123
      }
1124
      help.run();
2✔
1125
      return 1;
4✔
1126
    } catch (Throwable t) {
1✔
1127
      this.startContext.activateLogging();
3✔
1128
      step.error(t, true);
4✔
1129
      throw t;
2✔
1130
    } finally {
1131
      step.close();
2✔
1132
      assert (this.currentStep == null);
4!
1133
      step.logSummary(supressStepSuccess);
3✔
1134
    }
1135
  }
1136

1137
  public void configureJavaUtilLogging() {
1138

1139
    if (this.julConfigured) {
3✔
1140
      return;
1✔
1141
    }
1142
    Properties properties = createJavaUtilLoggingProperties();
3✔
1143
    if (properties == null) {
2!
1144
      return;
×
1145
    }
1146
    try {
1147
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1148
      properties.store(out, null);
4✔
1149
      out.flush();
2✔
1150
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1151
      LogManager.getLogManager().readConfiguration(in);
3✔
1152
      this.julConfigured = true;
3✔
1153
    } catch (IOException e) {
×
1154
      error(e);
×
1155
    }
1✔
1156
  }
1✔
1157

1158
  protected Properties createJavaUtilLoggingProperties() {
1159
    boolean logfile = false;
×
1160
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1161
    if (Boolean.TRUE.equals(writeLogfile)) {
×
1162
      logfile = true;
×
1163
      this.startContext.setWriteLogfile(true);
×
1164
    }
1165
    return createJavaUtilLoggingProperties(false, logfile);
×
1166
  }
1167

1168
  protected final Properties createJavaUtilLoggingProperties(boolean console, boolean file) {
1169

1170
    Path idePath = getIdePath();
3✔
1171
    if (file && (idePath == null)) {
2!
1172
      file = false;
×
1173
      error("Cannot enable --logfile option since IDE_ROOT is undefined.");
×
1174
    }
1175
    if (!console && !file) {
2!
1176
      return null;
×
1177
    }
1178
    Properties properties = new Properties();
4✔
1179
    String level = "FINE";
2✔
1180
    if (trace().isEnabled()) {
4!
1181
      level = "FINER";
2✔
1182
    }
1183
    // java.util.logging is so flawed: We cannot set the root level to a higher threshold than our own logger, instead we need to list all potential loggers
1184
    properties.setProperty(".level", level);
5✔
1185
    String extLevel = "WARNING";
2✔
1186
    properties.setProperty("org.level", extLevel);
5✔
1187
    properties.setProperty("java.level", extLevel);
5✔
1188
    properties.setProperty("java.lang.level", extLevel);
5✔
1189
    properties.setProperty("java.lang.FooBar.level", extLevel);
5✔
1190
    // properties.setProperty("java.lang.ProcessBuilder.level", extLevel);
1191
    properties.setProperty("jdk.level", extLevel);
5✔
1192
    properties.setProperty("net.level", extLevel);
5✔
1193
    properties.setProperty("io.level", extLevel);
5✔
1194
    properties.setProperty("sf.level", extLevel);
5✔
1195
    properties.setProperty("sun.level", extLevel);
5✔
1196
    if (file && console) {
2!
1197
      properties.setProperty("handlers", "java.util.logging.ConsoleHandler,java.util.logging.FileHandler");
×
1198
    } else if (file) {
2!
1199
      properties.setProperty("handlers", "java.util.logging.FileHandler");
×
1200
    } else {
1201
      properties.setProperty("handlers", "java.util.logging.ConsoleHandler");
5✔
1202
    }
1203
    if (file) {
2!
1204
      properties.setProperty("java.util.logging.FileHandler.level", level);
×
1205
      properties.setProperty("java.util.logging.FileHandler.formatter", "java.util.logging.SimpleFormatter");
×
1206
      properties.setProperty("java.util.logging.FileHandler.encoding", "UTF-8");
×
1207
      LocalDateTime now = LocalDateTime.now();
×
1208
      Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1209
      getFileAccess().mkdirs(logsPath);
×
1210
      properties.setProperty("java.util.logging.FileHandler.pattern", logsPath.resolve("ideasy-" + DateTimeUtil.formatTime(now) + ".log").toString());
×
1211
    }
1212
    if (console) {
2!
1213
      properties.setProperty("java.util.logging.ConsoleHandler.level", level);
5✔
1214
      properties.setProperty("java.util.logging.ConsoleHandler.formatter", "java.util.logging.SimpleFormatter");
5✔
1215
      properties.setProperty("java.util.logging.ConsoleHandler.encoding", "UTF-8");
5✔
1216
    }
1217
    properties.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%4$s] [%3$s] %5$s%6$s%n");
5✔
1218
    return properties;
2✔
1219
  }
1220

1221
  @Override
1222
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1223

1224
    this.startContext.deactivateLogging(threshold);
4✔
1225
    lambda.run();
2✔
1226
    this.startContext.activateLogging();
3✔
1227
  }
1✔
1228

1229
  /**
1230
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1231
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1232
   *     {@link Commandlet} did not match and we have to try a different candidate).
1233
   */
1234
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1235

1236
    IdeLogLevel previousLogLevel = null;
2✔
1237
    cmd.reset();
2✔
1238
    ValidationResult result = apply(arguments, cmd);
5✔
1239
    if (result.isValid()) {
3!
1240
      result = cmd.validate();
3✔
1241
    }
1242
    if (result.isValid()) {
3!
1243
      debug("Running commandlet {}", cmd);
9✔
1244
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1245
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1246
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1247
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1248
      }
1249
      try {
1250
        if (cmd.isProcessableOutput()) {
3!
1251
          if (!debug().isEnabled()) {
×
1252
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1253
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1254
          }
1255
          this.startContext.activateLogging();
×
1256
        } else {
1257
          this.startContext.activateLogging();
3✔
1258
          if (cmd.isIdeHomeRequired()) {
3!
1259
            debug(getMessageIdeHomeFound());
4✔
1260
          }
1261
          Path settingsRepository = getSettingsGitRepository();
3✔
1262
          if (settingsRepository != null) {
2!
1263
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1264
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1265
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1266
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1267
                interaction(
×
1268
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1269
              } else {
1270
                interaction(
×
1271
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1272
              }
1273
            }
1274
          }
1275
        }
1276
        boolean success = ensureLicenseAgreement(cmd);
4✔
1277
        if (!success) {
2!
1278
          return ValidationResultValid.get();
×
1279
        }
1280
        cmd.run();
2✔
1281
      } finally {
1282
        this.julConfigured = false;
3✔
1283
        if (previousLogLevel != null) {
2!
1284
          this.startContext.setLogLevel(previousLogLevel);
×
1285
        }
1286
      }
1✔
1287
    } else {
1288
      trace("Commandlet did not match");
×
1289
    }
1290
    return result;
2✔
1291
  }
1292

1293
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1294

1295
    if (isTest()) {
3!
1296
      return true; // ignore for tests
2✔
1297
    }
1298
    getFileAccess().mkdirs(this.userHomeIde);
×
1299
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1300
    if (Files.isRegularFile(licenseAgreement)) {
×
1301
      return true; // success, license already accepted
×
1302
    }
1303
    if (cmd instanceof EnvironmentCommandlet) {
×
1304
      // if the license was not accepted, "$(ideasy env --bash)" that is written into a variable prevents the user from seeing the question he is asked
1305
      // in such situation the user could not open a bash terminal anymore and gets blocked what would really annoy the user so we exit here without doing or
1306
      // printing anything anymore in such case.
1307
      return false;
×
1308
    }
1309
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1310
    if (logLevelInfoDisabled) {
×
1311
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1312
    }
1313
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1314
    if (logLevelInteractionDisabled) {
×
1315
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1316
    }
1317
    StringBuilder sb = new StringBuilder(1180);
×
1318
    sb.append(LOGO).append("""
×
1319
        Welcome to IDEasy!
1320
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1321
        It supports automatic download and installation of arbitrary 3rd party tools.
1322
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1323
        But if explicitly configured, also commercial software that requires an additional license may be used.
1324
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1325
        You are solely responsible for all risks implied by using this software.
1326
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1327
        You will be able to find it online under the following URL:
1328
        """).append(LICENSE_URL);
×
1329
    if (this.ideRoot != null) {
×
1330
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1331
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1332
    }
1333
    info(sb.toString());
×
1334
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1335

1336
    sb.setLength(0);
×
1337
    LocalDateTime now = LocalDateTime.now();
×
1338
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1339
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1340
    try {
1341
      Files.writeString(licenseAgreement, sb);
×
1342
    } catch (Exception e) {
×
1343
      throw new RuntimeException("Failed to save license agreement!", e);
×
1344
    }
×
1345
    if (logLevelInfoDisabled) {
×
1346
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1347
    }
1348
    if (logLevelInteractionDisabled) {
×
1349
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1350
    }
1351
    return true;
×
1352
  }
1353

1354
  @Override
1355
  public void verifyIdeMinVersion(boolean throwException) {
1356
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1357
    if (minVersion == null) {
2✔
1358
      return;
1✔
1359
    }
1360
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1361
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1362
          + "However, this is too old as your project requires at latest version %s\n"
1363
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1364
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1365
      if (throwException) {
2✔
1366
        throw new CliException(message);
5✔
1367
      } else {
1368
        warning(message);
3✔
1369
      }
1370
    }
1371
  }
1✔
1372

1373
  /**
1374
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1375
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1376
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1377
   */
1378
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1379

1380
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1381
    if (arguments.current().isStart()) {
4✔
1382
      arguments.next();
3✔
1383
    }
1384
    if (includeContextOptions) {
2✔
1385
      ContextCommandlet cc = new ContextCommandlet();
4✔
1386
      for (Property<?> property : cc.getProperties()) {
11✔
1387
        assert (property.isOption());
4!
1388
        property.apply(arguments, this, cc, collector);
7✔
1389
      }
1✔
1390
    }
1391
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1392
    CliArgument current = arguments.current();
3✔
1393
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1394
      collector.add(current.get(), null, null, null);
7✔
1395
    }
1396
    arguments.next();
3✔
1397
    while (commandletIterator.hasNext()) {
3✔
1398
      Commandlet cmd = commandletIterator.next();
4✔
1399
      if (!arguments.current().isEnd()) {
4✔
1400
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1401
      }
1402
    }
1✔
1403
    return collector.getSortedCandidates();
3✔
1404
  }
1405

1406
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1407

1408
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1409
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1410
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1411
    List<Property<?>> properties = cmd.getProperties();
3✔
1412
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1413
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1414
    for (Property<?> property : properties) {
10✔
1415
      if (property.isOption()) {
3✔
1416
        optionProperties.add(property);
4✔
1417
      }
1418
    }
1✔
1419
    CliArgument currentArgument = arguments.current();
3✔
1420
    while (!currentArgument.isEnd()) {
3✔
1421
      trace("Trying to match argument '{}'", currentArgument);
9✔
1422
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1423
        if (currentArgument.isCompletion()) {
3✔
1424
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1425
          while (optionIterator.hasNext()) {
3✔
1426
            Property<?> option = optionIterator.next();
4✔
1427
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1428
            if (success) {
2✔
1429
              optionIterator.remove();
2✔
1430
              arguments.next();
3✔
1431
            }
1432
          }
1✔
1433
        } else {
1✔
1434
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1435
          if (option != null) {
2✔
1436
            arguments.next();
3✔
1437
            boolean removed = optionProperties.remove(option);
4✔
1438
            if (!removed) {
2!
1439
              option = null;
×
1440
            }
1441
          }
1442
          if (option == null) {
2✔
1443
            trace("No such option was found.");
3✔
1444
            return;
1✔
1445
          }
1446
        }
1✔
1447
      } else {
1448
        if (valueIterator.hasNext()) {
3✔
1449
          Property<?> valueProperty = valueIterator.next();
4✔
1450
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1451
          if (!success) {
2✔
1452
            trace("Completion cannot match any further.");
3✔
1453
            return;
1✔
1454
          }
1455
        } else {
1✔
1456
          trace("No value left for completion.");
3✔
1457
          return;
1✔
1458
        }
1459
      }
1460
      currentArgument = arguments.current();
4✔
1461
    }
1462
  }
1✔
1463

1464
  /**
1465
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1466
   *     {@link CliArguments#copy() copy} as needed.
1467
   * @param cmd the potential {@link Commandlet} to match.
1468
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1469
   */
1470
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1471

1472
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1473
    CliArgument currentArgument = arguments.current();
3✔
1474
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1475
    Property<?> property = null;
2✔
1476
    if (propertyIterator.hasNext()) {
3!
1477
      property = propertyIterator.next();
4✔
1478
    }
1479
    while (!currentArgument.isEnd()) {
3✔
1480
      trace("Trying to match argument '{}'", currentArgument);
9✔
1481
      Property<?> currentProperty = property;
2✔
1482
      if (!arguments.isEndOptions()) {
3!
1483
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1484
        if (option != null) {
2!
1485
          currentProperty = option;
×
1486
        }
1487
      }
1488
      if (currentProperty == null) {
2!
1489
        trace("No option or next value found");
×
1490
        ValidationState state = new ValidationState(null);
×
1491
        state.addErrorMessage("No matching property found");
×
1492
        return state;
×
1493
      }
1494
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1495
      if (currentProperty == property) {
3!
1496
        if (!property.isMultiValued()) {
3✔
1497
          if (propertyIterator.hasNext()) {
3✔
1498
            property = propertyIterator.next();
5✔
1499
          } else {
1500
            property = null;
2✔
1501
          }
1502
        }
1503
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1504
          arguments.stopSplitShortOptions();
2✔
1505
        }
1506
      }
1507
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1508
      if (!matches) {
2!
1509
        ValidationState state = new ValidationState(null);
×
1510
        state.addErrorMessage("No matching property found");
×
1511
        return state;
×
1512
      }
1513
      currentArgument = arguments.current();
3✔
1514
    }
1✔
1515
    return ValidationResultValid.get();
2✔
1516
  }
1517

1518
  @Override
1519
  public Path findBash() {
1520
    if (this.bash != null) {
3✔
1521
      return this.bash;
3✔
1522
    }
1523
    Path bashPath = findBashOnBashPath();
3✔
1524
    if (bashPath == null) {
2✔
1525
      bashPath = findBashInPath();
3✔
1526
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1527
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1528
        if (bashPath == null) {
2!
1529
          bashPath = findBashInWindowsRegistry();
3✔
1530
        }
1531
      }
1532
    }
1533
    if (bashPath == null) {
2✔
1534
      error("No bash executable could be found on your system.");
4✔
1535
    } else {
1536
      this.bash = bashPath;
3✔
1537
    }
1538
    return bashPath;
2✔
1539
  }
1540

1541
  private Path findBashOnBashPath() {
1542
    trace("Trying to find BASH_PATH environment variable.");
3✔
1543
    Path bash;
1544
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1545
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1546
    if (bashVariable != null) {
2✔
1547
      bash = Path.of(bashVariable);
5✔
1548
      if (Files.exists(bash)) {
5✔
1549
        debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
13✔
1550
        return bash;
2✔
1551
      } else {
1552
        error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
13✔
1553
        return null;
2✔
1554
      }
1555
    } else {
1556
      debug("{} environment variable was not found", bashPathVariableName);
9✔
1557
      return null;
2✔
1558
    }
1559
  }
1560

1561
  /**
1562
   * @param path the path to check.
1563
   * @param toIgnore the String sequence which needs to be checked and ignored.
1564
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1565
   */
1566
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1567
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1568
    return !s.contains(toIgnore);
7!
1569
  }
1570

1571
  /**
1572
   * Tries to find the bash.exe within the PATH environment variable.
1573
   *
1574
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1575
   */
1576
  private Path findBashInPath() {
1577
    trace("Trying to find bash in PATH environment variable.");
3✔
1578
    Path bash;
1579
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1580
    if (pathVariableName != null) {
2!
1581
      Path plainBash = Path.of(BASH);
5✔
1582
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1583
          "\\windows\\system32");
1584
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1585
      bash = bashPath.toAbsolutePath();
3✔
1586
      if (bashPath.equals(plainBash)) {
4✔
1587
        warning("No usable bash executable was found in your PATH environment variable!");
3✔
1588
        bash = null;
3✔
1589
      } else {
1590
        if (Files.exists(bashPath)) {
5!
1591
          debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
10✔
1592
        } else {
1593
          bash = null;
×
1594
          error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1595
        }
1596
      }
1597
    } else {
1✔
1598
      bash = null;
×
1599
      // this should never happen...
1600
      error("PATH environment variable was not found");
×
1601
    }
1602
    return bash;
2✔
1603
  }
1604

1605
  /**
1606
   * Tries to find the bash.exe within the Windows registry.
1607
   *
1608
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1609
   */
1610
  protected Path findBashInWindowsRegistry() {
1611
    trace("Trying to find bash in Windows registry");
×
1612
    // If not found in the default location, try the registry query
1613
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1614
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1615
    for (String bashVariant : bashVariants) {
×
1616
      trace("Trying to find bash variant: {}", bashVariant);
×
1617
      for (String registryKey : registryKeys) {
×
1618
        trace("Trying to find bash from registry key: {}", registryKey);
×
1619
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1620
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1621

1622
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1623
        if (path != null) {
×
1624
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1625
          if (Files.exists(bashPath)) {
×
1626
            debug("Found bash at: {}", bashPath);
×
1627
            return bashPath;
×
1628
          } else {
1629
            error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1630
            return null;
×
1631
          }
1632
        } else {
1633
          info("No bash executable could be found in the Windows registry.");
×
1634
        }
1635
      }
1636
    }
1637
    // no bash found
1638
    return null;
×
1639
  }
1640

1641
  private Path findBashOnWindowsDefaultGitPath() {
1642
    // Check if Git Bash exists in the default location
1643
    trace("Trying to find bash on the Windows default git path.");
3✔
1644
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1645
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1646
      trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1647
      return defaultPath;
×
1648
    }
1649
    debug("No bash was found on the Windows default git path.");
3✔
1650
    return null;
2✔
1651
  }
1652

1653
  @Override
1654
  public WindowsPathSyntax getPathSyntax() {
1655

1656
    return this.pathSyntax;
3✔
1657
  }
1658

1659
  /**
1660
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1661
   */
1662
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1663

1664
    this.pathSyntax = pathSyntax;
3✔
1665
  }
1✔
1666

1667
  /**
1668
   * @return the {@link IdeStartContextImpl}.
1669
   */
1670
  public IdeStartContextImpl getStartContext() {
1671

1672
    return startContext;
3✔
1673
  }
1674

1675
  /**
1676
   * @return the {@link WindowsHelper}.
1677
   */
1678
  public final WindowsHelper getWindowsHelper() {
1679

1680
    if (this.windowsHelper == null) {
3✔
1681
      this.windowsHelper = createWindowsHelper();
4✔
1682
    }
1683
    return this.windowsHelper;
3✔
1684
  }
1685

1686
  /**
1687
   * @return the new {@link WindowsHelper} instance.
1688
   */
1689
  protected WindowsHelper createWindowsHelper() {
1690

1691
    return new WindowsHelperImpl(this);
×
1692
  }
1693

1694
  /**
1695
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1696
   */
1697
  public void reload() {
1698

1699
    this.variables = null;
3✔
1700
    this.customToolRepository = null;
3✔
1701
  }
1✔
1702

1703
  @Override
1704
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1705

1706
    assert (Files.isDirectory(installationPath));
6!
1707
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1708
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1709
  }
1✔
1710

1711
  /*
1712
   * @param home the IDE_HOME directory.
1713
   * @param workspace the name of the active workspace folder.
1714
   */
1715
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1716

1717
  }
1718

1719
  /**
1720
   * Returns the default git path on Windows. Required to be overwritten in tests.
1721
   *
1722
   * @return default path to git on Windows.
1723
   */
1724
  public String getDefaultWindowsGitPath() {
1725
    return DEFAULT_WINDOWS_GIT_PATH;
×
1726
  }
1727

1728
}
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