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

devonfw / IDEasy / 13009540527

28 Jan 2025 11:34AM UTC coverage: 68.41% (-0.04%) from 68.45%
13009540527

Pull #760

github

web-flow
Merge b4669bb35 into 05af7d1fc
Pull Request #760: #404: logging concept

2819 of 4513 branches covered (62.46%)

Branch coverage included in aggregate %.

7307 of 10289 relevant lines covered (71.02%)

3.08 hits per line

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

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

3
import java.io.BufferedReader;
4
import java.io.InputStreamReader;
5
import java.net.URL;
6
import java.net.URLConnection;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.time.LocalDateTime;
10
import java.util.ArrayList;
11
import java.util.HashMap;
12
import java.util.Iterator;
13
import java.util.List;
14
import java.util.Locale;
15
import java.util.Map;
16

17
import com.devonfw.tools.ide.cli.CliAbortException;
18
import com.devonfw.tools.ide.cli.CliArgument;
19
import com.devonfw.tools.ide.cli.CliArguments;
20
import com.devonfw.tools.ide.cli.CliException;
21
import com.devonfw.tools.ide.commandlet.Commandlet;
22
import com.devonfw.tools.ide.commandlet.CommandletManager;
23
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
24
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
25
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
26
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
27
import com.devonfw.tools.ide.common.SystemPath;
28
import com.devonfw.tools.ide.completion.CompletionCandidate;
29
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
30
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
31
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
32
import com.devonfw.tools.ide.environment.EnvironmentVariables;
33
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
34
import com.devonfw.tools.ide.environment.IdeSystem;
35
import com.devonfw.tools.ide.environment.IdeSystemImpl;
36
import com.devonfw.tools.ide.git.GitContext;
37
import com.devonfw.tools.ide.git.GitContextImpl;
38
import com.devonfw.tools.ide.git.GitUrl;
39
import com.devonfw.tools.ide.io.FileAccess;
40
import com.devonfw.tools.ide.io.FileAccessImpl;
41
import com.devonfw.tools.ide.log.IdeLogLevel;
42
import com.devonfw.tools.ide.log.IdeLogger;
43
import com.devonfw.tools.ide.log.IdeSubLogger;
44
import com.devonfw.tools.ide.merge.DirectoryMerger;
45
import com.devonfw.tools.ide.network.NetworkProxy;
46
import com.devonfw.tools.ide.os.SystemInfo;
47
import com.devonfw.tools.ide.os.SystemInfoImpl;
48
import com.devonfw.tools.ide.os.WindowsPathSyntax;
49
import com.devonfw.tools.ide.process.ProcessContext;
50
import com.devonfw.tools.ide.process.ProcessContextImpl;
51
import com.devonfw.tools.ide.process.ProcessResult;
52
import com.devonfw.tools.ide.property.Property;
53
import com.devonfw.tools.ide.repo.CustomToolRepository;
54
import com.devonfw.tools.ide.repo.CustomToolRepositoryImpl;
55
import com.devonfw.tools.ide.repo.DefaultToolRepository;
56
import com.devonfw.tools.ide.repo.ToolRepository;
57
import com.devonfw.tools.ide.step.Step;
58
import com.devonfw.tools.ide.step.StepImpl;
59
import com.devonfw.tools.ide.url.model.UrlMetadata;
60
import com.devonfw.tools.ide.util.DateTimeUtil;
61
import com.devonfw.tools.ide.validation.ValidationResult;
62
import com.devonfw.tools.ide.validation.ValidationResultValid;
63
import com.devonfw.tools.ide.validation.ValidationState;
64
import com.devonfw.tools.ide.variable.IdeVariables;
65

66
/**
67
 * Abstract base implementation of {@link IdeContext}.
68
 */
69
public abstract class AbstractIdeContext implements IdeContext {
70

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

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

75
  private final IdeStartContextImpl startContext;
76

77
  private Path ideHome;
78

79
  private final Path ideRoot;
80

81
  private Path confPath;
82

83
  protected Path settingsPath;
84

85
  private Path settingsCommitIdPath;
86

87
  private Path softwarePath;
88

89
  private Path softwareExtraPath;
90

91
  private final Path softwareRepositoryPath;
92

93
  protected Path pluginsPath;
94

95
  private Path workspacePath;
96

97
  private String workspaceName;
98

99
  protected Path urlsPath;
100

101
  private final Path tempPath;
102

103
  private final Path tempDownloadPath;
104

105
  private Path cwd;
106

107
  private Path downloadPath;
108

109
  private final Path toolRepositoryPath;
110

111
  protected Path userHome;
112

113
  private Path userHomeIde;
114

115
  private SystemPath path;
116

117
  private WindowsPathSyntax pathSyntax;
118

119
  private final SystemInfo systemInfo;
120

121
  private EnvironmentVariables variables;
122

123
  private final FileAccess fileAccess;
124

125
  protected CommandletManager commandletManager;
126

127
  protected ToolRepository defaultToolRepository;
128

129
  private CustomToolRepository customToolRepository;
130

131
  private DirectoryMerger workspaceMerger;
132

133
  protected UrlMetadata urlMetadata;
134

135
  protected Path defaultExecutionDirectory;
136

137
  private StepImpl currentStep;
138

139
  protected Boolean online;
140

141
  protected IdeSystem system;
142

143
  private NetworkProxy networkProxy;
144

145
  /** Context used for logging */
146
  public static IdeContext loggingContext;
147

148
  /**
149
   * The constructor.
150
   *
151
   * @param startContext the {@link IdeLogger}.
152
   * @param workingDirectory the optional {@link Path} to current working directory.
153
   */
154
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
155

156
    super();
2✔
157
    this.startContext = startContext;
3✔
158
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
159
    this.commandletManager = new CommandletManagerImpl(this);
6✔
160
    this.fileAccess = new FileAccessImpl(this);
6✔
161
    String workspace = WORKSPACE_MAIN;
2✔
162
    if (workingDirectory == null) {
2!
163
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
164
    } else {
165
      workingDirectory = workingDirectory.toAbsolutePath();
3✔
166
    }
167
    // detect IDE_HOME and WORKSPACE
168
    Path currentDir = workingDirectory;
2✔
169
    String name1 = "";
2✔
170
    String name2 = "";
2✔
171
    while (currentDir != null) {
2✔
172
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
173
      if (isIdeHome(currentDir)) {
4✔
174
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
175
          workspace = name2;
3✔
176
        }
177
        break;
178
      }
179
      name2 = name1;
2✔
180
      int nameCount = currentDir.getNameCount();
3✔
181
      if (nameCount >= 1) {
3✔
182
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
183
      }
184
      currentDir = currentDir.getParent();
3✔
185
    }
1✔
186

187
    // detection completed, initializing variables
188
    this.ideRoot = findIdeRoot(currentDir);
5✔
189

190
    setCwd(workingDirectory, workspace, currentDir);
5✔
191

192
    if (this.ideRoot == null) {
3✔
193
      this.toolRepositoryPath = null;
3✔
194
      this.urlsPath = null;
3✔
195
      this.tempPath = null;
3✔
196
      this.tempDownloadPath = null;
3✔
197
      this.softwareRepositoryPath = null;
4✔
198
    } else {
199
      Path ideBase = this.ideRoot.resolve(FOLDER_IDE);
5✔
200
      this.toolRepositoryPath = ideBase.resolve("software");
5✔
201
      this.urlsPath = ideBase.resolve("urls");
5✔
202
      this.tempPath = ideBase.resolve("tmp");
5✔
203
      this.tempDownloadPath = this.tempPath.resolve(FOLDER_DOWNLOADS);
6✔
204
      this.softwareRepositoryPath = ideBase.resolve(FOLDER_SOFTWARE);
5✔
205
      if (Files.isDirectory(this.tempPath)) {
7✔
206
        // TODO delete all files older than 1 day here...
207
      } else {
208
        this.fileAccess.mkdirs(this.tempDownloadPath);
5✔
209
      }
210
    }
211

212
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
213
    loggingContext = this;
2✔
214
  }
1✔
215

216
  private Path findIdeRoot(Path ideHomePath) {
217

218
    Path ideRootPath = null;
2✔
219
    if (ideHomePath != null) {
2✔
220
      ideRootPath = ideHomePath.getParent();
4✔
221
    } else if (!isTest()) {
3!
222
      ideRootPath = getIdeRootPathFromEnv();
×
223
    }
224
    return ideRootPath;
2✔
225
  }
226

227
  private Path getIdeRootPathFromEnv() {
228

229
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
230
    if (root != null) {
×
231
      Path rootPath = Path.of(root);
×
232
      if (Files.isDirectory(rootPath)) {
×
233
        return rootPath;
×
234
      }
235
    }
236
    return null;
×
237
  }
238

239
  @Override
240
  public void setCwd(Path userDir, String workspace, Path ideHome) {
241

242
    this.cwd = userDir;
3✔
243
    this.workspaceName = workspace;
3✔
244
    this.ideHome = ideHome;
3✔
245
    if (ideHome == null) {
2✔
246
      this.workspacePath = null;
3✔
247
      this.confPath = null;
3✔
248
      this.settingsPath = null;
3✔
249
      this.softwarePath = null;
3✔
250
      this.softwareExtraPath = null;
3✔
251
      this.pluginsPath = null;
4✔
252
    } else {
253
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
254
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
255
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
256
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
257
      this.softwarePath = this.ideHome.resolve(FOLDER_SOFTWARE);
6✔
258
      this.softwareExtraPath = this.softwarePath.resolve(FOLDER_EXTRA);
6✔
259
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
260
    }
261
    if (isTest()) {
3!
262
      // only for testing...
263
      if (this.ideHome == null) {
3✔
264
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
265
      } else {
266
        this.userHome = this.ideHome.resolve("home");
7✔
267
      }
268
    } else {
269
      this.userHome = Path.of(getSystem().getProperty("user.home"));
×
270
    }
271
    this.userHomeIde = this.userHome.resolve(".ide");
6✔
272
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
273

274
    this.path = computeSystemPath();
4✔
275
  }
1✔
276

277
  private String getMessageIdeHomeFound() {
278

279
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
280
  }
281

282
  private String getMessageIdeHomeNotFound() {
283

284
    return "You are not inside an IDE installation: " + this.cwd;
5✔
285
  }
286

287
  private String getMessageIdeRootNotFound() {
288

289
    String root = getSystem().getEnv("IDE_ROOT");
5✔
290
    if (root == null) {
2!
291
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
292
    } else {
293
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
294
    }
295
  }
296

297
  /**
298
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
299
   */
300
  public boolean isTest() {
301

302
    return false;
×
303
  }
304

305
  protected SystemPath computeSystemPath() {
306

307
    return new SystemPath(this);
×
308
  }
309

310
  private boolean isIdeHome(Path dir) {
311

312
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
313
      return false;
2✔
314
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
315
      return false;
×
316
    }
317
    return true;
2✔
318
  }
319

320
  private EnvironmentVariables createVariables() {
321

322
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
323
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
324
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
325
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
326
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
327
    return conf.resolved();
3✔
328
  }
329

330
  protected AbstractEnvironmentVariables createSystemVariables() {
331

332
    return EnvironmentVariables.ofSystem(this);
3✔
333
  }
334

335
  @Override
336
  public SystemInfo getSystemInfo() {
337

338
    return this.systemInfo;
3✔
339
  }
340

341
  @Override
342
  public FileAccess getFileAccess() {
343

344
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
345
    configureNetworkProxy();
2✔
346
    return this.fileAccess;
3✔
347
  }
348

349
  @Override
350
  public CommandletManager getCommandletManager() {
351

352
    return this.commandletManager;
3✔
353
  }
354

355
  @Override
356
  public ToolRepository getDefaultToolRepository() {
357

358
    return this.defaultToolRepository;
3✔
359
  }
360

361
  @Override
362
  public CustomToolRepository getCustomToolRepository() {
363

364
    if (this.customToolRepository == null) {
3!
365
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
366
    }
367
    return this.customToolRepository;
3✔
368
  }
369

370
  @Override
371
  public Path getIdeHome() {
372

373
    return this.ideHome;
3✔
374
  }
375

376
  @Override
377
  public String getProjectName() {
378

379
    if (this.ideHome != null) {
3!
380
      return this.ideHome.getFileName().toString();
5✔
381
    }
382
    return "";
×
383
  }
384

385
  @Override
386
  public Path getIdeRoot() {
387

388
    return this.ideRoot;
3✔
389
  }
390

391
  @Override
392
  public Path getCwd() {
393

394
    return this.cwd;
3✔
395
  }
396

397
  @Override
398
  public Path getTempPath() {
399

400
    return this.tempPath;
3✔
401
  }
402

403
  @Override
404
  public Path getTempDownloadPath() {
405

406
    return this.tempDownloadPath;
3✔
407
  }
408

409
  @Override
410
  public Path getUserHome() {
411

412
    return this.userHome;
3✔
413
  }
414

415
  @Override
416
  public Path getUserHomeIde() {
417

418
    return this.userHomeIde;
3✔
419
  }
420

421
  @Override
422
  public Path getSettingsPath() {
423

424
    return this.settingsPath;
3✔
425
  }
426

427
  @Override
428
  public Path getSettingsGitRepository() {
429

430
    Path settingsPath = getSettingsPath();
3✔
431

432
    if (settingsPath == null) {
2✔
433
      error("No settings repository was found.");
3✔
434
      return null;
2✔
435
    }
436

437
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
438
    if (!Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
10!
439
      error("Settings repository exists but is not a git repository.");
3✔
440
      return null;
2✔
441
    }
442

443
    return settingsPath;
×
444
  }
445

446
  public boolean isSettingsRepositorySymlinkOrJunction() {
447

448
    Path settingsPath = getSettingsPath();
3✔
449
    if (settingsPath == null) {
2!
450
      return false;
×
451
    }
452
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
453
  }
454

455
  @Override
456
  public Path getSettingsCommitIdPath() {
457

458
    return this.settingsCommitIdPath;
3✔
459
  }
460

461
  @Override
462
  public Path getConfPath() {
463

464
    return this.confPath;
3✔
465
  }
466

467
  @Override
468
  public Path getSoftwarePath() {
469

470
    return this.softwarePath;
3✔
471
  }
472

473
  @Override
474
  public Path getSoftwareExtraPath() {
475

476
    return this.softwareExtraPath;
3✔
477
  }
478

479
  @Override
480
  public Path getSoftwareRepositoryPath() {
481

482
    return this.softwareRepositoryPath;
3✔
483
  }
484

485
  @Override
486
  public Path getPluginsPath() {
487

488
    return this.pluginsPath;
3✔
489
  }
490

491
  @Override
492
  public String getWorkspaceName() {
493

494
    return this.workspaceName;
3✔
495
  }
496

497
  @Override
498
  public Path getWorkspacePath() {
499

500
    return this.workspacePath;
3✔
501
  }
502

503
  @Override
504
  public Path getDownloadPath() {
505

506
    return this.downloadPath;
3✔
507
  }
508

509
  @Override
510
  public Path getUrlsPath() {
511

512
    return this.urlsPath;
3✔
513
  }
514

515
  @Override
516
  public Path getToolRepositoryPath() {
517

518
    return this.toolRepositoryPath;
3✔
519
  }
520

521
  @Override
522
  public SystemPath getPath() {
523

524
    return this.path;
3✔
525
  }
526

527
  @Override
528
  public EnvironmentVariables getVariables() {
529

530
    if (this.variables == null) {
3✔
531
      this.variables = createVariables();
4✔
532
    }
533
    return this.variables;
3✔
534
  }
535

536
  @Override
537
  public UrlMetadata getUrls() {
538

539
    if (this.urlMetadata == null) {
3✔
540
      if (!isTest()) {
3!
541
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, this.urlsPath, null);
×
542
      }
543
      this.urlMetadata = new UrlMetadata(this);
6✔
544
    }
545
    return this.urlMetadata;
3✔
546
  }
547

548
  @Override
549
  public boolean isQuietMode() {
550

551
    return this.startContext.isQuietMode();
4✔
552
  }
553

554
  @Override
555
  public boolean isBatchMode() {
556

557
    return this.startContext.isBatchMode();
×
558
  }
559

560
  @Override
561
  public boolean isForceMode() {
562

563
    return this.startContext.isForceMode();
4✔
564
  }
565

566
  @Override
567
  public boolean isOfflineMode() {
568

569
    return this.startContext.isOfflineMode();
4✔
570
  }
571

572
  @Override
573
  public boolean isSkipUpdatesMode() {
574

575
    return this.startContext.isSkipUpdatesMode();
×
576
  }
577

578
  @Override
579
  public boolean isOnline() {
580

581
    if (this.online == null) {
3✔
582
      configureNetworkProxy();
2✔
583
      // we currently assume we have only a CLI process that runs shortly
584
      // therefore we run this check only once to save resources when this method is called many times
585
      try {
586
        int timeout = 1000;
2✔
587
        //open a connection to github.com and try to retrieve data
588
        //getContent fails if there is no connection
589
        URLConnection connection = new URL("https://www.github.com").openConnection();
6✔
590
        connection.setConnectTimeout(timeout);
3✔
591
        connection.getContent();
3✔
592
        this.online = Boolean.TRUE;
3✔
593
      } catch (Exception ignored) {
×
594
        this.online = Boolean.FALSE;
×
595
      }
1✔
596
    }
597
    return this.online.booleanValue();
4✔
598
  }
599

600
  private void configureNetworkProxy() {
601

602
    if (this.networkProxy == null) {
3✔
603
      this.networkProxy = new NetworkProxy(this);
6✔
604
      this.networkProxy.configure();
3✔
605
    }
606
  }
1✔
607

608
  @Override
609
  public Locale getLocale() {
610

611
    Locale locale = this.startContext.getLocale();
4✔
612
    if (locale == null) {
2!
613
      locale = Locale.getDefault();
×
614
    }
615
    return locale;
2✔
616
  }
617

618
  @Override
619
  public DirectoryMerger getWorkspaceMerger() {
620

621
    if (this.workspaceMerger == null) {
3✔
622
      this.workspaceMerger = new DirectoryMerger(this);
6✔
623
    }
624
    return this.workspaceMerger;
3✔
625
  }
626

627
  /**
628
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
629
   */
630
  @Override
631
  public Path getDefaultExecutionDirectory() {
632

633
    return this.defaultExecutionDirectory;
×
634
  }
635

636
  /**
637
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
638
   */
639
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
640

641
    if (defaultExecutionDirectory != null) {
×
642
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
643
    }
644
  }
×
645

646
  @Override
647
  public GitContext getGitContext() {
648

649
    return new GitContextImpl(this);
×
650
  }
651

652
  @Override
653
  public ProcessContext newProcess() {
654

655
    ProcessContext processContext = createProcessContext();
3✔
656
    if (this.defaultExecutionDirectory != null) {
3!
657
      processContext.directory(this.defaultExecutionDirectory);
×
658
    }
659
    return processContext;
2✔
660
  }
661

662
  @Override
663
  public IdeSystem getSystem() {
664

665
    if (this.system == null) {
×
666
      this.system = new IdeSystemImpl(this);
×
667
    }
668
    return this.system;
×
669
  }
670

671
  /**
672
   * @return a new instance of {@link ProcessContext}.
673
   * @see #newProcess()
674
   */
675
  protected ProcessContext createProcessContext() {
676

677
    return new ProcessContextImpl(this);
×
678
  }
679

680
  @Override
681
  public IdeSubLogger level(IdeLogLevel level) {
682

683
    return this.startContext.level(level);
5✔
684
  }
685

686
  @Override
687
  public void logIdeHomeAndRootStatus() {
688

689
    if (this.ideRoot != null) {
3!
690
      success("IDE_ROOT is set to {}", this.ideRoot);
×
691
    }
692
    if (this.ideHome == null) {
3!
693
      warning(getMessageIdeHomeNotFound());
5✔
694
    } else {
695
      success("IDE_HOME is set to {}", this.ideHome);
×
696
    }
697
  }
1✔
698

699
  @Override
700
  public String askForInput(String message, String defaultValue) {
701

702
    if (!message.isBlank()) {
×
703
      info(message);
×
704
    }
705
    if (isBatchMode()) {
×
706
      if (isForceMode()) {
×
707
        return defaultValue;
×
708
      } else {
709
        throw new CliAbortException();
×
710
      }
711
    }
712
    String input = readLine().trim();
×
713
    return input.isEmpty() ? defaultValue : input;
×
714
  }
715

716
  @Override
717
  public String askForInput(String message) {
718

719
    String input;
720
    do {
721
      info(message);
3✔
722
      input = readLine().trim();
4✔
723
    } while (input.isEmpty());
3!
724

725
    return input;
2✔
726
  }
727

728
  @SuppressWarnings("unchecked")
729
  @Override
730
  public <O> O question(String question, O... options) {
731

732
    assert (options.length >= 2);
×
733
    interaction(question);
×
734
    Map<String, O> mapping = new HashMap<>(options.length);
×
735
    int i = 0;
×
736
    for (O option : options) {
×
737
      i++;
×
738
      String key = "" + option;
×
739
      addMapping(mapping, key, option);
×
740
      String numericKey = Integer.toString(i);
×
741
      if (numericKey.equals(key)) {
×
742
        trace("Options should not be numeric: " + key);
×
743
      } else {
744
        addMapping(mapping, numericKey, option);
×
745
      }
746
      interaction("Option " + numericKey + ": " + key);
×
747
    }
748
    O option = null;
×
749
    if (isBatchMode()) {
×
750
      if (isForceMode()) {
×
751
        option = options[0];
×
752
        interaction("" + option);
×
753
      }
754
    } else {
755
      while (option == null) {
×
756
        String answer = readLine();
×
757
        option = mapping.get(answer);
×
758
        if (option == null) {
×
759
          warning("Invalid answer: '" + answer + "' - please try again.");
×
760
        }
761
      }
×
762
    }
763
    return option;
×
764
  }
765

766
  /**
767
   * @return the input from the end-user (e.g. read from the console).
768
   */
769
  protected abstract String readLine();
770

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

773
    O duplicate = mapping.put(key, option);
×
774
    if (duplicate != null) {
×
775
      throw new IllegalArgumentException("Duplicated option " + key);
×
776
    }
777
  }
×
778

779
  @Override
780
  public Step getCurrentStep() {
781

782
    return this.currentStep;
×
783
  }
784

785
  @Override
786
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
787

788
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
789
    return this.currentStep;
3✔
790
  }
791

792
  /**
793
   * Internal method to end the running {@link Step}.
794
   *
795
   * @param step the current {@link Step} to end.
796
   */
797
  public void endStep(StepImpl step) {
798

799
    if (step == this.currentStep) {
4!
800
      this.currentStep = this.currentStep.getParent();
6✔
801
    } else {
802
      String currentStepName = "null";
×
803
      if (this.currentStep != null) {
×
804
        currentStepName = this.currentStep.getName();
×
805
      }
806
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
807
    }
808
  }
1✔
809

810
  /**
811
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
812
   *
813
   * @param arguments the {@link CliArgument}.
814
   * @return the return code of the execution.
815
   */
816
  public int run(CliArguments arguments) {
817

818
    CliArgument current = arguments.current();
3✔
819
    assert (this.currentStep == null);
4!
820
    boolean supressStepSuccess = false;
2✔
821
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
822
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
823
    Commandlet cmd = null;
2✔
824
    ValidationResult result = null;
2✔
825
    try {
826
      while (commandletIterator.hasNext()) {
3!
827
        cmd = commandletIterator.next();
4✔
828
        result = applyAndRun(arguments.copy(), cmd);
6✔
829
        if (result.isValid()) {
3!
830
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
831
          step.success();
2✔
832
          return ProcessResult.SUCCESS;
4✔
833
        }
834
      }
835
      this.startContext.activateLogging();
×
836
      if (result != null) {
×
837
        error(result.getErrorMessage());
×
838
      }
839
      step.error("Invalid arguments: {}", current.getArgs());
×
840
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
×
841
      if (cmd != null) {
×
842
        help.commandlet.setValue(cmd);
×
843
      }
844
      help.run();
×
845
      return 1;
×
846
    } catch (Throwable t) {
1✔
847
      this.startContext.activateLogging();
3✔
848
      step.error(t, true);
4✔
849
      throw t;
2✔
850
    } finally {
851
      step.close();
2✔
852
      assert (this.currentStep == null);
4!
853
      step.logSummary(supressStepSuccess);
3✔
854
    }
855
  }
856

857
  /**
858
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
859
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
860
   *     {@link Commandlet} did not match and we have to try a different candidate).
861
   */
862
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
863

864
    IdeLogLevel previousLogLevel = null;
2✔
865
    cmd.reset();
2✔
866
    ValidationResult result = apply(arguments, cmd);
5✔
867
    if (result.isValid()) {
3!
868
      result = cmd.validate();
3✔
869
    }
870
    if (result.isValid()) {
3!
871
      debug("Running commandlet {}", cmd);
9✔
872
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
873
        throw new CliException(getMessageIdeHomeNotFound(), ProcessResult.NO_IDE_HOME);
×
874
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6✔
875
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
876
      }
877
      try {
878
        if (cmd.isProcessableOutput()) {
3!
879
          if (!debug().isEnabled()) {
×
880
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
881
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
882
          }
883
          this.startContext.activateLogging();
×
884
        } else {
885
          this.startContext.activateLogging();
3✔
886
          verifyIdeRoot();
2✔
887
          if (cmd.isIdeHomeRequired()) {
3✔
888
            debug(getMessageIdeHomeFound());
4✔
889
          }
890
          Path settingsRepository = getSettingsGitRepository();
3✔
891
          if (settingsRepository != null) {
2!
892
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
893
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
894
                    settingsRepository, getSettingsCommitIdPath()))) {
×
895
              if (isSettingsRepositorySymlinkOrJunction()) {
×
896
                interaction(
×
897
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
898

899
              } else {
900
                interaction(
×
901
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
902
              }
903
            }
904
          }
905
        }
906
        boolean success = ensureLicenseAgreement(cmd);
4✔
907
        if (!success) {
2!
908
          return ValidationResultValid.get();
×
909
        }
910
        cmd.run();
2✔
911
      } finally {
912
        if (previousLogLevel != null) {
2!
913
          this.startContext.setLogLevel(previousLogLevel);
×
914
        }
915
      }
1✔
916
    } else {
917
      trace("Commandlet did not match");
×
918
    }
919
    return result;
2✔
920
  }
921

922
  private boolean ensureLicenseAgreement(Commandlet cmd) {
923

924
    if (isTest()) {
3!
925
      return true; // ignore for tests
2✔
926
    }
927
    getFileAccess().mkdirs(this.userHomeIde);
×
928
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
929
    if (Files.isRegularFile(licenseAgreement)) {
×
930
      return true; // success, license already accepted
×
931
    }
932
    if (cmd instanceof EnvironmentCommandlet) {
×
933
      // 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
934
      // 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
935
      // printing anything anymore in such case.
936
      return false;
×
937
    }
938
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
939
    if (logLevelInfoDisabled) {
×
940
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
941
    }
942
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
943
    if (logLevelInteractionDisabled) {
×
944
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
945
    }
946
    StringBuilder sb = new StringBuilder(1180);
×
947
    sb.append(LOGO).append("""
×
948
        Welcome to IDEasy!
949
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
950
        It supports automatic download and installation of arbitrary 3rd party tools.
951
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
952
        But if explicitly configured, also commercial software that requires an additional license may be used.
953
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
954
        You are solely responsible for all risks implied by using this software.
955
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
956
        You will be able to find it online under the following URL:
957
        """).append(LICENSE_URL);
×
958
    if (this.ideRoot != null) {
×
959
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
960
          append(this.ideRoot.resolve(FOLDER_IDE).resolve("IDEasy.pdf").toString()).append("\n");
×
961
    }
962
    info(sb.toString());
×
963
    askToContinue("Do you accept these terms of use and all license agreements?");
×
964

965
    sb.setLength(0);
×
966
    LocalDateTime now = LocalDateTime.now();
×
967
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
968
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
969
    try {
970
      Files.writeString(licenseAgreement, sb);
×
971
    } catch (Exception e) {
×
972
      throw new RuntimeException("Failed to save license agreement!", e);
×
973
    }
×
974
    if (logLevelInfoDisabled) {
×
975
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
976
    }
977
    if (logLevelInteractionDisabled) {
×
978
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
979
    }
980
    return true;
×
981
  }
982

983
  private void verifyIdeRoot() {
984

985
    if (!isTest()) {
3!
986
      if (this.ideRoot == null) {
×
987
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
988
      } else if (this.ideHome != null) {
×
989
        Path ideRootPath = getIdeRootPathFromEnv();
×
990
        if (!this.ideRoot.equals(ideRootPath)) {
×
991
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath,
×
992
              this.ideHome.getFileName(), this.ideRoot);
×
993
        }
994
      }
995
    }
996
  }
1✔
997

998
  /**
999
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1000
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1001
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1002
   */
1003
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1004

1005
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1006
    if (arguments.current().isStart()) {
4✔
1007
      arguments.next();
3✔
1008
    }
1009
    if (includeContextOptions) {
2✔
1010
      ContextCommandlet cc = new ContextCommandlet();
4✔
1011
      for (Property<?> property : cc.getProperties()) {
11✔
1012
        assert (property.isOption());
4!
1013
        property.apply(arguments, this, cc, collector);
7✔
1014
      }
1✔
1015
    }
1016
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1017
    CliArgument current = arguments.current();
3✔
1018
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1019
      collector.add(current.get(), null, null, null);
7✔
1020
    }
1021
    arguments.next();
3✔
1022
    while (commandletIterator.hasNext()) {
3✔
1023
      Commandlet cmd = commandletIterator.next();
4✔
1024
      if (!arguments.current().isEnd()) {
4✔
1025
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1026
      }
1027
    }
1✔
1028
    return collector.getSortedCandidates();
3✔
1029
  }
1030

1031
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1032

1033
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1034
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1035
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1036
    List<Property<?>> properties = cmd.getProperties();
3✔
1037
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1038
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1039
    for (Property<?> property : properties) {
10✔
1040
      if (property.isOption()) {
3✔
1041
        optionProperties.add(property);
4✔
1042
      }
1043
    }
1✔
1044
    CliArgument currentArgument = arguments.current();
3✔
1045
    while (!currentArgument.isEnd()) {
3✔
1046
      trace("Trying to match argument '{}'", currentArgument);
9✔
1047
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1048
        if (currentArgument.isCompletion()) {
3✔
1049
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1050
          while (optionIterator.hasNext()) {
3✔
1051
            Property<?> option = optionIterator.next();
4✔
1052
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1053
            if (success) {
2✔
1054
              optionIterator.remove();
2✔
1055
              arguments.next();
3✔
1056
            }
1057
          }
1✔
1058
        } else {
1✔
1059
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1060
          if (option != null) {
2✔
1061
            arguments.next();
3✔
1062
            boolean removed = optionProperties.remove(option);
4✔
1063
            if (!removed) {
2!
1064
              option = null;
×
1065
            }
1066
          }
1067
          if (option == null) {
2✔
1068
            trace("No such option was found.");
3✔
1069
            return;
1✔
1070
          }
1071
        }
1✔
1072
      } else {
1073
        if (valueIterator.hasNext()) {
3✔
1074
          Property<?> valueProperty = valueIterator.next();
4✔
1075
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1076
          if (!success) {
2✔
1077
            trace("Completion cannot match any further.");
3✔
1078
            return;
1✔
1079
          }
1080
        } else {
1✔
1081
          trace("No value left for completion.");
3✔
1082
          return;
1✔
1083
        }
1084
      }
1085
      currentArgument = arguments.current();
4✔
1086
    }
1087
  }
1✔
1088

1089
  /**
1090
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1091
   *     {@link CliArguments#copy() copy} as needed.
1092
   * @param cmd the potential {@link Commandlet} to match.
1093
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1094
   */
1095
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1096

1097
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1098
    CliArgument currentArgument = arguments.current();
3✔
1099
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1100
    Property<?> property = null;
2✔
1101
    if (propertyIterator.hasNext()) {
3!
1102
      property = propertyIterator.next();
4✔
1103
    }
1104
    while (!currentArgument.isEnd()) {
3✔
1105
      trace("Trying to match argument '{}'", currentArgument);
9✔
1106
      Property<?> currentProperty = property;
2✔
1107
      if (!arguments.isEndOptions()) {
3!
1108
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1109
        if (option != null) {
2!
1110
          currentProperty = option;
×
1111
        }
1112
      }
1113
      if (currentProperty == null) {
2!
1114
        trace("No option or next value found");
×
1115
        ValidationState state = new ValidationState(null);
×
1116
        state.addErrorMessage("No matching property found");
×
1117
        return state;
×
1118
      }
1119
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1120
      if (currentProperty == property) {
3!
1121
        if (!property.isMultiValued()) {
3✔
1122
          if (propertyIterator.hasNext()) {
3✔
1123
            property = propertyIterator.next();
5✔
1124
          } else {
1125
            property = null;
2✔
1126
          }
1127
        }
1128
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1129
          arguments.stopSplitShortOptions();
2✔
1130
        }
1131
      }
1132
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1133
      if (!matches && currentArgument.isCompletion()) {
2!
1134
        ValidationState state = new ValidationState(null);
×
1135
        state.addErrorMessage("No matching property found");
×
1136
        return state;
×
1137
      }
1138
      currentArgument = arguments.current();
3✔
1139
    }
1✔
1140
    return ValidationResultValid.get();
2✔
1141
  }
1142

1143
  @Override
1144
  public String findBash() {
1145

1146
    String bash = "bash";
2✔
1147
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1148
      bash = findBashOnWindows();
×
1149
    }
1150

1151
    return bash;
2✔
1152
  }
1153

1154
  private String findBashOnWindows() {
1155

1156
    // Check if Git Bash exists in the default location
1157
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1158
    if (Files.exists(defaultPath)) {
×
1159
      return defaultPath.toString();
×
1160
    }
1161

1162
    // If not found in the default location, try the registry query
1163
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1164
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1165
    String regQueryResult;
1166
    for (String bashVariant : bashVariants) {
×
1167
      for (String registryKey : registryKeys) {
×
1168
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1169
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1170

1171
        try {
1172
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1173
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1174
            StringBuilder output = new StringBuilder();
×
1175
            String line;
1176

1177
            while ((line = reader.readLine()) != null) {
×
1178
              output.append(line);
×
1179
            }
1180

1181
            int exitCode = process.waitFor();
×
1182
            if (exitCode != 0) {
×
1183
              return null;
×
1184
            }
1185

1186
            regQueryResult = output.toString();
×
1187
            if (regQueryResult != null) {
×
1188
              int index = regQueryResult.indexOf("REG_SZ");
×
1189
              if (index != -1) {
×
1190
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1191
                return path + "\\bin\\bash.exe";
×
1192
              }
1193
            }
1194

1195
          }
×
1196
        } catch (Exception e) {
×
1197
          return null;
×
1198
        }
×
1199
      }
1200
    }
1201
    // no bash found
1202
    return null;
×
1203
  }
1204

1205
  @Override
1206
  public WindowsPathSyntax getPathSyntax() {
1207

1208
    return this.pathSyntax;
3✔
1209
  }
1210

1211
  /**
1212
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1213
   */
1214
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1215

1216
    this.pathSyntax = pathSyntax;
3✔
1217
  }
1✔
1218

1219
  /**
1220
   * @return the {@link IdeStartContextImpl}.
1221
   */
1222
  public IdeStartContextImpl getStartContext() {
1223

1224
    return startContext;
3✔
1225
  }
1226

1227
  /**
1228
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1229
   */
1230
  public void reload() {
1231

1232
    this.variables = null;
3✔
1233
    this.customToolRepository = null;
3✔
1234
  }
1✔
1235
}
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