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

devonfw / IDEasy / 22860792264

09 Mar 2026 03:25PM UTC coverage: 70.268% (-0.2%) from 70.481%
22860792264

push

github

web-flow
#404: #1713: advanced logging (#1714)

Co-authored-by: Kian <adasd>
Co-authored-by: KianRolf <kian.loroff@capgemini.com>
Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>
Co-authored-by: jan-vcapgemini <jan-vincent.hoelzle@capgemini.com>

4068 of 6386 branches covered (63.7%)

Branch coverage included in aggregate %.

10604 of 14494 relevant lines covered (73.16%)

3.08 hits per line

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

65.91
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.FileHandler;
22
import java.util.logging.LogManager;
23
import java.util.logging.SimpleFormatter;
24

25
import org.slf4j.Logger;
26
import org.slf4j.LoggerFactory;
27

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

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

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

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

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

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

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

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

106
  private final IdeStartContextImpl startContext;
107

108
  private Path ideHome;
109

110
  private final Path ideRoot;
111

112
  private Path confPath;
113

114
  protected Path settingsPath;
115

116
  private Path settingsCommitIdPath;
117

118
  protected Path pluginsPath;
119

120
  private Path workspacePath;
121

122
  private String workspaceName;
123

124
  private Path cwd;
125

126
  private Path downloadPath;
127

128
  private Path userHome;
129

130
  private Path userHomeIde;
131

132
  private SystemPath path;
133

134
  private WindowsPathSyntax pathSyntax;
135

136
  private final SystemInfo systemInfo;
137

138
  private EnvironmentVariables variables;
139

140
  private final FileAccess fileAccess;
141

142
  protected CommandletManager commandletManager;
143

144
  protected ToolRepository defaultToolRepository;
145

146
  private CustomToolRepository customToolRepository;
147

148
  private MvnRepository mvnRepository;
149

150
  private NpmRepository npmRepository;
151

152
  private PipRepository pipRepository;
153

154
  private DirectoryMerger workspaceMerger;
155

156
  protected UrlMetadata urlMetadata;
157

158
  protected Path defaultExecutionDirectory;
159

160
  private StepImpl currentStep;
161

162
  private NetworkStatus networkStatus;
163

164
  protected IdeSystem system;
165

166
  private WindowsHelper windowsHelper;
167

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

170
  private Path bash;
171

172
  private boolean julConfigured;
173

174
  private Path logfile;
175

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

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

217
    // detection completed, initializing variables
218
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
219

220
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
221

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

233
  /**
234
   * 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
235
   * add additional validation or boundary checks.
236
   *
237
   * @param workingDirectory the starting directory for the search.
238
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
239
   */
240
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
241

242
    Path currentDir = workingDirectory;
2✔
243
    String name1 = "";
2✔
244
    String name2 = "";
2✔
245
    String workspace = WORKSPACE_MAIN;
2✔
246
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
247

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

268
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
269
  }
270

271
  /**
272
   * @return a new {@link MvnRepository}
273
   */
274
  protected MvnRepository createMvnRepository() {
275
    return new MvnRepository(this);
5✔
276
  }
277

278
  /**
279
   * @return a new {@link NpmRepository}
280
   */
281
  protected NpmRepository createNpmRepository() {
282
    return new NpmRepository(this);
×
283
  }
284

285
  /**
286
   * @return a new {@link PipRepository}
287
   */
288
  protected PipRepository createPipRepository() {
289
    return new PipRepository(this);
×
290
  }
291

292
  private Path findIdeRoot(Path ideHomePath) {
293

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

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

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

348
  @Override
349
  public void setCwd(Path userDir, String workspace, Path ideHome) {
350

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

380
  private String getMessageIdeHomeFound() {
381

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

389
  private String getMessageNotInsideIdeProject() {
390

391
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
392
  }
393

394
  private String getMessageIdeRootNotFound() {
395

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

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

410
    return false;
×
411
  }
412

413
  protected SystemPath computeSystemPath() {
414

415
    return new SystemPath(this);
×
416
  }
417

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

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

434
  private EnvironmentVariables createVariables() {
435

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

444
  protected AbstractEnvironmentVariables createSystemVariables() {
445

446
    return EnvironmentVariables.ofSystem(this);
3✔
447
  }
448

449
  @Override
450
  public SystemInfo getSystemInfo() {
451

452
    return this.systemInfo;
3✔
453
  }
454

455
  @Override
456
  public FileAccess getFileAccess() {
457

458
    return this.fileAccess;
3✔
459
  }
460

461
  @Override
462
  public CommandletManager getCommandletManager() {
463

464
    return this.commandletManager;
3✔
465
  }
466

467
  @Override
468
  public ToolRepository getDefaultToolRepository() {
469

470
    return this.defaultToolRepository;
3✔
471
  }
472

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

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

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

497
  @Override
498
  public CustomToolRepository getCustomToolRepository() {
499

500
    if (this.customToolRepository == null) {
3!
501
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
502
    }
503
    return this.customToolRepository;
3✔
504
  }
505

506
  @Override
507
  public Path getIdeHome() {
508

509
    return this.ideHome;
3✔
510
  }
511

512
  @Override
513
  public String getProjectName() {
514

515
    if (this.ideHome != null) {
3!
516
      return this.ideHome.getFileName().toString();
5✔
517
    }
518
    return "";
×
519
  }
520

521
  @Override
522
  public VersionIdentifier getProjectVersion() {
523

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

534
  @Override
535
  public void setProjectVersion(VersionIdentifier version) {
536

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

545
  @Override
546
  public Path getIdeRoot() {
547

548
    return this.ideRoot;
3✔
549
  }
550

551
  @Override
552
  public Path getIdePath() {
553

554
    Path myIdeRoot = getIdeRoot();
3✔
555
    if (myIdeRoot == null) {
2✔
556
      return null;
2✔
557
    }
558
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
559
  }
560

561
  @Override
562
  public Path getCwd() {
563

564
    return this.cwd;
3✔
565
  }
566

567
  @Override
568
  public Path getTempPath() {
569

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

577
  @Override
578
  public Path getTempDownloadPath() {
579

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

587
  @Override
588
  public Path getUserHome() {
589

590
    return this.userHome;
3✔
591
  }
592

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

600
    this.userHome = userHome;
3✔
601
    resetPrivacyMap();
2✔
602
  }
1✔
603

604
  @Override
605
  public Path getUserHomeIde() {
606

607
    return this.userHomeIde;
3✔
608
  }
609

610
  @Override
611
  public Path getSettingsPath() {
612

613
    return this.settingsPath;
3✔
614
  }
615

616
  @Override
617
  public Path getSettingsGitRepository() {
618

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

628
  @Override
629
  public boolean isSettingsRepositorySymlinkOrJunction() {
630

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

638
  @Override
639
  public Path getSettingsCommitIdPath() {
640

641
    return this.settingsCommitIdPath;
3✔
642
  }
643

644
  @Override
645
  public Path getConfPath() {
646

647
    return this.confPath;
3✔
648
  }
649

650
  @Override
651
  public Path getSoftwarePath() {
652

653
    if (this.ideHome == null) {
3✔
654
      return null;
2✔
655
    }
656
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
657
  }
658

659
  @Override
660
  public Path getSoftwareExtraPath() {
661

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

669
  @Override
670
  public Path getSoftwareRepositoryPath() {
671

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

679
  @Override
680
  public Path getPluginsPath() {
681

682
    return this.pluginsPath;
3✔
683
  }
684

685
  @Override
686
  public String getWorkspaceName() {
687

688
    return this.workspaceName;
3✔
689
  }
690

691
  @Override
692
  public Path getWorkspacePath() {
693

694
    return this.workspacePath;
3✔
695
  }
696

697
  @Override
698
  public Path getDownloadPath() {
699

700
    return this.downloadPath;
3✔
701
  }
702

703
  @Override
704
  public Path getUrlsPath() {
705

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

713
  @Override
714
  public Path getToolRepositoryPath() {
715

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

723
  @Override
724
  public SystemPath getPath() {
725

726
    return this.path;
3✔
727
  }
728

729
  @Override
730
  public EnvironmentVariables getVariables() {
731

732
    if (this.variables == null) {
3✔
733
      this.variables = createVariables();
4✔
734
    }
735
    return this.variables;
3✔
736
  }
737

738
  @Override
739
  public UrlMetadata getUrls() {
740

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

750
  @Override
751
  public boolean isQuietMode() {
752

753
    return this.startContext.isQuietMode();
4✔
754
  }
755

756
  @Override
757
  public boolean isBatchMode() {
758

759
    return this.startContext.isBatchMode();
4✔
760
  }
761

762
  @Override
763
  public boolean isForceMode() {
764

765
    return this.startContext.isForceMode();
4✔
766
  }
767

768
  @Override
769
  public boolean isForcePull() {
770

771
    return this.startContext.isForcePull();
4✔
772
  }
773

774
  @Override
775
  public boolean isForcePlugins() {
776

777
    return this.startContext.isForcePlugins();
4✔
778
  }
779

780
  @Override
781
  public boolean isForceRepositories() {
782

783
    return this.startContext.isForceRepositories();
4✔
784
  }
785

786
  @Override
787
  public boolean isOfflineMode() {
788

789
    return this.startContext.isOfflineMode();
4✔
790
  }
791

792
  @Override
793
  public boolean isPrivacyMode() {
794
    return this.startContext.isPrivacyMode();
4✔
795
  }
796

797
  @Override
798
  public boolean isSkipUpdatesMode() {
799

800
    return this.startContext.isSkipUpdatesMode();
4✔
801
  }
802

803
  @Override
804
  public boolean isNoColorsMode() {
805

806
    return this.startContext.isNoColorsMode();
×
807
  }
808

809
  @Override
810
  public NetworkStatus getNetworkStatus() {
811

812
    if (this.networkStatus == null) {
×
813
      this.networkStatus = new NetworkStatusImpl(this);
×
814
    }
815
    return this.networkStatus;
×
816
  }
817

818
  @Override
819
  public Locale getLocale() {
820

821
    Locale locale = this.startContext.getLocale();
4✔
822
    if (locale == null) {
2✔
823
      locale = Locale.getDefault();
2✔
824
    }
825
    return locale;
2✔
826
  }
827

828
  @Override
829
  public DirectoryMerger getWorkspaceMerger() {
830

831
    if (this.workspaceMerger == null) {
3✔
832
      this.workspaceMerger = new DirectoryMerger(this);
6✔
833
    }
834
    return this.workspaceMerger;
3✔
835
  }
836

837
  /**
838
   * @return the default execution directory in which a command process is executed.
839
   */
840
  @Override
841
  public Path getDefaultExecutionDirectory() {
842

843
    return this.defaultExecutionDirectory;
×
844
  }
845

846
  /**
847
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
848
   */
849
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
850

851
    if (defaultExecutionDirectory != null) {
×
852
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
853
    }
854
  }
×
855

856
  @Override
857
  public GitContext getGitContext() {
858

859
    return new GitContextImpl(this);
×
860
  }
861

862
  @Override
863
  public ProcessContext newProcess() {
864

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

872
  @Override
873
  public IdeSystem getSystem() {
874

875
    if (this.system == null) {
×
876
      this.system = new IdeSystemImpl();
×
877
    }
878
    return this.system;
×
879
  }
880

881
  /**
882
   * @return a new instance of {@link ProcessContext}.
883
   * @see #newProcess()
884
   */
885
  protected ProcessContext createProcessContext() {
886

887
    return new ProcessContextImpl(this);
×
888
  }
889

890
  @Override
891
  public IdeLogLevel getLogLevelConsole() {
892

893
    return this.startContext.getLogLevelConsole();
×
894
  }
895

896
  @Override
897
  public IdeLogLevel getLogLevelLogger() {
898

899
    return this.startContext.getLogLevelLogger();
×
900
  }
901

902
  @Override
903
  public IdeLogListener getLogListener() {
904

905
    return this.startContext.getLogListener();
×
906
  }
907

908
  @Override
909
  public void logIdeHomeAndRootStatus() {
910
    if (this.ideRoot != null) {
3!
911
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
912
    }
913
    if (this.ideHome == null) {
3✔
914
      LOG.warn(getMessageNotInsideIdeProject());
5✔
915
    } else {
916
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
917
    }
918
  }
1✔
919

920
  @Override
921
  public String formatArgument(Object argument) {
922

923
    if (argument == null) {
2✔
924
      return null;
2✔
925
    }
926
    String result = argument.toString();
3✔
927
    if (isPrivacyMode()) {
3✔
928
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
929
        initializePrivacyMap(this.userHome, "~");
×
930
        String projectName = getProjectName();
×
931
        if (!projectName.isEmpty()) {
×
932
          this.privacyMap.put(projectName, "project");
×
933
        }
934
      }
935
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
936
        result = result.replace(entry.getKey(), entry.getValue());
×
937
      }
×
938
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
939
    }
940
    return result;
2✔
941
  }
942

943
  /**
944
   * @param path the sensitive {@link Path} to
945
   * @param replacement the replacement to mask the {@link Path} in log output.
946
   */
947
  protected void initializePrivacyMap(Path path, String replacement) {
948

949
    if (path == null) {
×
950
      return;
×
951
    }
952
    if (this.systemInfo.isWindows()) {
×
953
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
954
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
955
    } else {
956
      this.privacyMap.put(path.toString(), replacement);
×
957
    }
958
  }
×
959

960
  /**
961
   * Resets the privacy map in case fundamental values have changed.
962
   */
963
  private void resetPrivacyMap() {
964

965
    this.privacyMap.clear();
3✔
966
  }
1✔
967

968

969
  @Override
970
  public String askForInput(String message, String defaultValue) {
971

972
    while (true) {
973
      if (!message.isBlank()) {
3!
974
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
975
      }
976
      if (isBatchMode()) {
3!
977
        if (isForceMode()) {
×
978
          return defaultValue;
×
979
        } else {
980
          throw new CliAbortException();
×
981
        }
982
      }
983
      String input = readLine().trim();
4✔
984
      if (!input.isEmpty()) {
3!
985
        return input;
2✔
986
      } else {
987
        if (defaultValue != null) {
×
988
          return defaultValue;
×
989
        }
990
      }
991
    }
×
992
  }
993

994
  @Override
995
  public <O> O question(O[] options, String question, Object... args) {
996

997
    assert (options.length > 0);
4!
998
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
999
    return displayOptionsAndGetAnswer(options);
4✔
1000
  }
1001

1002
  private <O> O displayOptionsAndGetAnswer(O[] options) {
1003
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
1004
    int i = 0;
2✔
1005
    for (O option : options) {
16✔
1006
      i++;
1✔
1007
      String title = "" + option;
4✔
1008
      String key = computeOptionKey(title);
3✔
1009
      addMapping(mapping, key, option);
4✔
1010
      String numericKey = Integer.toString(i);
3✔
1011
      if (numericKey.equals(key)) {
4!
1012
        LOG.trace("Options should not be numeric: {}", key);
×
1013
      } else {
1014
        addMapping(mapping, numericKey, option);
4✔
1015
      }
1016
      IdeLogLevel.INTERACTION.log(LOG, "Option {}: {}", numericKey, title);
14✔
1017
    }
1018
    O option = null;
2✔
1019
    if (isBatchMode()) {
3!
1020
      if (isForceMode()) {
×
1021
        option = options[0];
×
1022
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1023
      }
1024
    } else {
1025
      while (option == null) {
2✔
1026
        String answer = readLine();
3✔
1027
        option = mapping.get(answer);
4✔
1028
        if (option == null) {
2!
1029
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1030
        }
1031
      }
1✔
1032
    }
1033
    return option;
2✔
1034
  }
1035

1036
  private static String computeOptionKey(String option) {
1037
    String key = option;
2✔
1038
    int index = -1;
2✔
1039
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1040
      int currentIndex = key.indexOf(c);
4✔
1041
      if (currentIndex != -1) {
3✔
1042
        if ((index == -1) || (currentIndex < index)) {
3!
1043
          index = currentIndex;
2✔
1044
        }
1045
      }
1046
    }
1047
    if (index > 0) {
2✔
1048
      key = key.substring(0, index).trim();
6✔
1049
    }
1050
    return key;
2✔
1051
  }
1052

1053
  /**
1054
   * @return the input from the end-user (e.g. read from the console).
1055
   */
1056
  protected abstract String readLine();
1057

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

1060
    O duplicate = mapping.put(key, option);
5✔
1061
    if (duplicate != null) {
2!
1062
      throw new IllegalArgumentException("Duplicated option " + key);
×
1063
    }
1064
  }
1✔
1065

1066
  @Override
1067
  public Step getCurrentStep() {
1068

1069
    return this.currentStep;
×
1070
  }
1071

1072
  @Override
1073
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1074

1075
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1076
    return this.currentStep;
3✔
1077
  }
1078

1079
  /**
1080
   * Internal method to end the running {@link Step}.
1081
   *
1082
   * @param step the current {@link Step} to end.
1083
   */
1084
  public void endStep(StepImpl step) {
1085

1086
    if (step == this.currentStep) {
4!
1087
      this.currentStep = this.currentStep.getParent();
6✔
1088
    } else {
1089
      String currentStepName = "null";
×
1090
      if (this.currentStep != null) {
×
1091
        currentStepName = this.currentStep.getName();
×
1092
      }
1093
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1094
    }
1095
  }
1✔
1096

1097
  /**
1098
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1099
   *
1100
   * @param arguments the {@link CliArgument}.
1101
   * @return the return code of the execution.
1102
   */
1103
  public int run(CliArguments arguments) {
1104

1105
    CliArgument current = arguments.current();
3✔
1106
    assert (this.currentStep == null);
4!
1107
    boolean supressStepSuccess = false;
2✔
1108
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1109
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1110
    Commandlet cmd = null;
2✔
1111
    ValidationResult result = null;
2✔
1112
    try {
1113
      while (commandletIterator.hasNext()) {
3✔
1114
        cmd = commandletIterator.next();
4✔
1115
        result = applyAndRun(arguments.copy(), cmd);
6✔
1116
        if (result.isValid()) {
3!
1117
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1118
          step.success();
2✔
1119
          return ProcessResult.SUCCESS;
4✔
1120
        }
1121
      }
1122
      activateLogging(cmd);
3✔
1123
      verifyIdeMinVersion(false);
3✔
1124
      if (result != null) {
2!
1125
        LOG.error(result.getErrorMessage());
×
1126
      }
1127
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1128
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1129
      if (cmd != null) {
2!
1130
        help.commandlet.setValue(cmd);
×
1131
      }
1132
      help.run();
2✔
1133
      return 1;
4✔
1134
    } catch (Throwable t) {
1✔
1135
      activateLogging(cmd);
3✔
1136
      step.error(t, true);
4✔
1137
      if (this.logfile != null) {
3!
1138
        System.err.println("Logfile can be found at " + this.logfile); // do not use logger
×
1139
      }
1140
      throw t;
2✔
1141
    } finally {
1142
      step.close();
2✔
1143
      assert (this.currentStep == null);
4!
1144
      step.logSummary(supressStepSuccess);
3✔
1145
    }
1146
  }
1147

1148
  /**
1149
   * Ensure the logging system is initialized.
1150
   */
1151
  private void activateLogging(Commandlet cmd) {
1152

1153
    configureJavaUtilLogging(cmd);
3✔
1154
    this.startContext.activateLogging();
3✔
1155
  }
1✔
1156

1157
  /**
1158
   * Configures the logging system (JUL).
1159
   *
1160
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1161
   */
1162
  public void configureJavaUtilLogging(Commandlet cmd) {
1163

1164
    if (this.julConfigured) {
3✔
1165
      return;
1✔
1166
    }
1167
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1168
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1169
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1170
    try {
1171
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1172
      properties.store(out, null);
4✔
1173
      out.flush();
2✔
1174
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1175
      LogManager.getLogManager().readConfiguration(in);
3✔
1176
      this.julConfigured = true;
3✔
1177
      this.startContext.activateLogging();
3✔
1178
    } catch (IOException e) {
×
1179
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1180
    }
1✔
1181
  }
1✔
1182

1183
  protected boolean isWriteLogfile(Commandlet cmd) {
1184
    if (!cmd.isWriteLogFile()) {
×
1185
      return false;
×
1186
    }
1187
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1188
    return Boolean.TRUE.equals(writeLogfile);
×
1189
  }
1190

1191
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1192

1193
    Path idePath = getIdePath();
3✔
1194
    if (writeLogfile && (idePath == null)) {
2!
1195
      writeLogfile = false;
×
1196
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1197
    }
1198
    Properties properties = new Properties();
4✔
1199
    // prevent 3rd party (e.g. java.lang.ProcessBuilder) logging into our console via JUL
1200
    // see JulLogLevel for the trick we did to workaround JUL flaws
1201
    properties.setProperty(".level", "SEVERE");
5✔
1202
    if (writeLogfile) {
2!
1203
      this.startContext.setLogLevelLogger(IdeLogLevel.TRACE);
×
1204
      String fileHandlerName = FileHandler.class.getName();
×
1205
      properties.setProperty("handlers", JulConsoleHandler.class.getName() + "," + fileHandlerName);
×
1206
      properties.setProperty(fileHandlerName + ".formatter", SimpleFormatter.class.getName());
×
1207
      properties.setProperty(fileHandlerName + ".encoding", "UTF-8");
×
1208
      this.logfile = createLogfilePath(idePath, cmd);
×
1209
      getFileAccess().mkdirs(this.logfile.getParent());
×
1210
      properties.setProperty(fileHandlerName + ".pattern", this.logfile.toString());
×
1211
    } else {
×
1212
      properties.setProperty("handlers", JulConsoleHandler.class.getName());
6✔
1213
    }
1214
    properties.setProperty(SimpleFormatter.class.getName() + ".format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%4$s] [%3$s] %5$s%6$s%n");
7✔
1215
    return properties;
2✔
1216
  }
1217

1218
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1219
    LocalDateTime now = LocalDateTime.now();
×
1220
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1221
    StringBuilder sb = new StringBuilder(32);
×
1222
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1223
      sb.append("_ide-");
×
1224
    } else {
1225
      sb.append(this.ideHome.getFileName().toString());
×
1226
      sb.append('-');
×
1227
    }
1228
    sb.append("ide-");
×
1229
    if (cmd != null) {
×
1230
      sb.append(cmd.getName());
×
1231
      sb.append('-');
×
1232
    }
1233
    sb.append(DateTimeUtil.formatTime(now));
×
1234
    sb.append(".log");
×
1235
    return logsPath.resolve(sb.toString());
×
1236
  }
1237

1238
  @Override
1239
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1240

1241
    this.startContext.deactivateLogging(threshold);
4✔
1242
    lambda.run();
2✔
1243
    this.startContext.activateLogging();
3✔
1244
  }
1✔
1245

1246
  /**
1247
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1248
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1249
   *     {@link Commandlet} did not match and we have to try a different candidate).
1250
   */
1251
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1252

1253
    IdeLogLevel previousLogLevel = null;
2✔
1254
    cmd.reset();
2✔
1255
    ValidationResult result = apply(arguments, cmd);
5✔
1256
    if (result.isValid()) {
3!
1257
      result = cmd.validate();
3✔
1258
    }
1259
    if (result.isValid()) {
3!
1260
      LOG.debug("Running commandlet {}", cmd);
4✔
1261
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1262
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1263
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1264
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1265
      }
1266
      try {
1267
        if (cmd.isProcessableOutput()) {
3!
1268
          if (!LOG.isDebugEnabled()) {
×
1269
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1270
            previousLogLevel = this.startContext.setLogLevelConsole(IdeLogLevel.PROCESSABLE);
×
1271
          }
1272
        } else {
1273
          if (cmd.isIdeHomeRequired()) {
3!
1274
            LOG.debug(getMessageIdeHomeFound());
4✔
1275
          }
1276
          Path settingsRepository = getSettingsGitRepository();
3✔
1277
          if (settingsRepository != null) {
2!
1278
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1279
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1280
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1281
              String msg;
1282
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1283
                msg = "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1284
              } else {
1285
                msg = "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1286
              }
1287
              IdeLogLevel.INTERACTION.log(LOG, msg);
×
1288
            }
1289
          }
1290
        }
1291
        boolean success = ensureLicenseAgreement(cmd);
4✔
1292
        if (!success) {
2!
1293
          return ValidationResultValid.get();
×
1294
        }
1295
        cmd.run();
2✔
1296
      } finally {
1297
        if (previousLogLevel != null) {
2!
1298
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1299
        }
1300
      }
1✔
1301
    } else {
1302
      LOG.trace("Commandlet did not match");
×
1303
    }
1304
    return result;
2✔
1305
  }
1306

1307
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1308

1309
    if (isTest()) {
3!
1310
      return true; // ignore for tests
2✔
1311
    }
1312
    getFileAccess().mkdirs(this.userHomeIde);
×
1313
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1314
    if (Files.isRegularFile(licenseAgreement)) {
×
1315
      return true; // success, license already accepted
×
1316
    }
1317
    if (cmd instanceof EnvironmentCommandlet) {
×
1318
      // 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
1319
      // 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
1320
      // printing anything anymore in such case.
1321
      return false;
×
1322
    }
1323
    IdeLogLevel oldLogLevel = this.startContext.getLogLevelConsole();
×
1324
    IdeLogLevel newLogLevel = oldLogLevel;
×
1325
    if (oldLogLevel.ordinal() > IdeLogLevel.INFO.ordinal()) {
×
1326
      newLogLevel = IdeLogLevel.INFO;
×
1327
      this.startContext.setLogLevelConsole(newLogLevel);
×
1328
    }
1329
    StringBuilder sb = new StringBuilder(1180);
×
1330
    sb.append(LOGO).append("""
×
1331
        Welcome to IDEasy!
1332
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1333
        It supports automatic download and installation of arbitrary 3rd party tools.
1334
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1335
        But if explicitly configured, also commercial software that requires an additional license may be used.
1336
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1337
        You are solely responsible for all risks implied by using this software.
1338
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1339
        You will be able to find it online under the following URL:
1340
        """).append(LICENSE_URL);
×
1341
    if (this.ideRoot != null) {
×
1342
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1343
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1344
    }
1345
    LOG.info(sb.toString());
×
1346
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1347

1348
    sb.setLength(0);
×
1349
    LocalDateTime now = LocalDateTime.now();
×
1350
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1351
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1352
    try {
1353
      Files.writeString(licenseAgreement, sb);
×
1354
    } catch (Exception e) {
×
1355
      throw new RuntimeException("Failed to save license agreement!", e);
×
1356
    }
×
1357
    if (oldLogLevel != newLogLevel) {
×
1358
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1359
    }
1360
    return true;
×
1361
  }
1362

1363
  @Override
1364
  public void verifyIdeMinVersion(boolean throwException) {
1365
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1366
    if (minVersion == null) {
2✔
1367
      return;
1✔
1368
    }
1369
    VersionIdentifier versionIdentifier = IdeVersion.getVersionIdentifier();
2✔
1370
    if (versionIdentifier.compareVersion(minVersion).isLess() && !IdeVersion.isUndefined()) {
7!
1371
      String message = String.format("Your version of IDEasy is currently %s\n"
13✔
1372
          + "However, this is too old as your project requires at latest version %s\n"
1373
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1374
          + "ide upgrade", versionIdentifier, minVersion);
1375
      if (throwException) {
2✔
1376
        throw new CliException(message);
5✔
1377
      } else {
1378
        LOG.warn(message);
3✔
1379
      }
1380
    }
1381
  }
1✔
1382

1383
  /**
1384
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1385
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1386
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1387
   */
1388
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1389

1390
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1391
    if (arguments.current().isStart()) {
4✔
1392
      arguments.next();
3✔
1393
    }
1394
    if (includeContextOptions) {
2✔
1395
      ContextCommandlet cc = new ContextCommandlet();
4✔
1396
      for (Property<?> property : cc.getProperties()) {
11✔
1397
        assert (property.isOption());
4!
1398
        property.apply(arguments, this, cc, collector);
7✔
1399
      }
1✔
1400
    }
1401
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1402
    CliArgument current = arguments.current();
3✔
1403
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1404
      collector.add(current.get(), null, null, null);
7✔
1405
    }
1406
    arguments.next();
3✔
1407
    while (commandletIterator.hasNext()) {
3✔
1408
      Commandlet cmd = commandletIterator.next();
4✔
1409
      if (!arguments.current().isEnd()) {
4✔
1410
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1411
      }
1412
    }
1✔
1413
    return collector.getSortedCandidates();
3✔
1414
  }
1415

1416
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1417

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

1474
  /**
1475
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1476
   *     {@link CliArguments#copy() copy} as needed.
1477
   * @param cmd the potential {@link Commandlet} to match.
1478
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1479
   */
1480
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1481

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

1528
  @Override
1529
  public Path findBash() {
1530
    if (this.bash != null) {
3✔
1531
      return this.bash;
3✔
1532
    }
1533
    Path bashPath = findBashOnBashPath();
3✔
1534
    if (bashPath == null) {
2✔
1535
      bashPath = findBashInPath();
3✔
1536
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1537
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1538
        if (bashPath == null) {
2!
1539
          bashPath = findBashInWindowsRegistry();
3✔
1540
        }
1541
      }
1542
    }
1543
    if (bashPath == null) {
2✔
1544
      LOG.error("No bash executable could be found on your system.");
4✔
1545
    } else {
1546
      this.bash = bashPath;
3✔
1547
    }
1548
    return bashPath;
2✔
1549
  }
1550

1551
  private Path findBashOnBashPath() {
1552
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1553
    Path bash;
1554
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1555
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1556
    if (bashVariable != null) {
2✔
1557
      bash = Path.of(bashVariable);
5✔
1558
      if (Files.exists(bash)) {
5✔
1559
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1560
        return bash;
2✔
1561
      } else {
1562
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1563
        return null;
2✔
1564
      }
1565
    } else {
1566
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1567
      return null;
2✔
1568
    }
1569
  }
1570

1571
  /**
1572
   * @param path the path to check.
1573
   * @param toIgnore the String sequence which needs to be checked and ignored.
1574
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1575
   */
1576
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1577
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1578
    return !s.contains(toIgnore);
7!
1579
  }
1580

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

1615
  /**
1616
   * Tries to find the bash.exe within the Windows registry.
1617
   *
1618
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1619
   */
1620
  protected Path findBashInWindowsRegistry() {
1621
    LOG.trace("Trying to find bash in Windows registry");
×
1622
    // If not found in the default location, try the registry query
1623
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1624
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1625
    for (String bashVariant : bashVariants) {
×
1626
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1627
      for (String registryKey : registryKeys) {
×
1628
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1629
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1630
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1631

1632
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1633
        if (path != null) {
×
1634
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1635
          if (Files.exists(bashPath)) {
×
1636
            LOG.debug("Found bash at: {}", bashPath);
×
1637
            return bashPath;
×
1638
          } else {
1639
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1640
            return null;
×
1641
          }
1642
        } else {
1643
          LOG.info("No bash executable could be found in the Windows registry.");
×
1644
        }
1645
      }
1646
    }
1647
    // no bash found
1648
    return null;
×
1649
  }
1650

1651
  private Path findBashOnWindowsDefaultGitPath() {
1652
    // Check if Git Bash exists in the default location
1653
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1654
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1655
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1656
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1657
      return defaultPath;
×
1658
    }
1659
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1660
    return null;
2✔
1661
  }
1662

1663
  @Override
1664
  public WindowsPathSyntax getPathSyntax() {
1665

1666
    return this.pathSyntax;
3✔
1667
  }
1668

1669
  /**
1670
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1671
   */
1672
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1673

1674
    this.pathSyntax = pathSyntax;
3✔
1675
  }
1✔
1676

1677
  /**
1678
   * @return the {@link IdeStartContextImpl}.
1679
   */
1680
  public IdeStartContextImpl getStartContext() {
1681

1682
    return startContext;
3✔
1683
  }
1684

1685
  /**
1686
   * @return the {@link WindowsHelper}.
1687
   */
1688
  public final WindowsHelper getWindowsHelper() {
1689

1690
    if (this.windowsHelper == null) {
3✔
1691
      this.windowsHelper = createWindowsHelper();
4✔
1692
    }
1693
    return this.windowsHelper;
3✔
1694
  }
1695

1696
  /**
1697
   * @return the new {@link WindowsHelper} instance.
1698
   */
1699
  protected WindowsHelper createWindowsHelper() {
1700

1701
    return new WindowsHelperImpl(this);
×
1702
  }
1703

1704
  /**
1705
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1706
   */
1707
  public void reload() {
1708

1709
    this.variables = null;
3✔
1710
    this.customToolRepository = null;
3✔
1711
  }
1✔
1712

1713
  @Override
1714
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1715

1716
    assert (Files.isDirectory(installationPath));
6!
1717
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1718
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1719
  }
1✔
1720

1721
  /*
1722
   * @param home the IDE_HOME directory.
1723
   * @param workspace the name of the active workspace folder.
1724
   */
1725
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1726

1727
  }
1728

1729
  /**
1730
   * Returns the default git path on Windows. Required to be overwritten in tests.
1731
   *
1732
   * @return default path to git on Windows.
1733
   */
1734
  public String getDefaultWindowsGitPath() {
1735
    return DEFAULT_WINDOWS_GIT_PATH;
×
1736
  }
1737

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