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

devonfw / IDEasy / 12952020921

24 Jan 2025 02:55PM UTC coverage: 68.401% (-0.04%) from 68.444%
12952020921

Pull #983

github

web-flow
Merge c8dfff2e7 into 307966958
Pull Request #983: #931: enhance settings in code repository

2789 of 4473 branches covered (62.35%)

Branch coverage included in aggregate %.

7205 of 10138 relevant lines covered (71.07%)

3.09 hits per line

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

58.99
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
  /**
146
   * The constructor.
147
   *
148
   * @param startContext the {@link IdeLogger}.
149
   * @param workingDirectory the optional {@link Path} to current working directory.
150
   */
151
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
152

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

184
    // detection completed, initializing variables
185
    this.ideRoot = findIdeRoot(currentDir);
5✔
186

187
    setCwd(workingDirectory, workspace, currentDir);
5✔
188

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

209
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
210
  }
1✔
211

212
  private Path findIdeRoot(Path ideHomePath) {
213

214
    Path ideRootPath = null;
2✔
215
    if (ideHomePath != null) {
2✔
216
      ideRootPath = ideHomePath.getParent();
4✔
217
    } else if (!isTest()) {
3!
218
      ideRootPath = getIdeRootPathFromEnv();
×
219
    }
220
    return ideRootPath;
2✔
221
  }
222

223
  private Path getIdeRootPathFromEnv() {
224

225
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
226
    if (root != null) {
×
227
      Path rootPath = Path.of(root);
×
228
      if (Files.isDirectory(rootPath)) {
×
229
        return rootPath;
×
230
      }
231
    }
232
    return null;
×
233
  }
234

235
  @Override
236
  public void setCwd(Path userDir, String workspace, Path ideHome) {
237

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

270
    this.path = computeSystemPath();
4✔
271
  }
1✔
272

273
  private String getMessageIdeHomeFound() {
274

275
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
276
  }
277

278
  private String getMessageIdeHomeNotFound() {
279

280
    return "You are not inside an IDE installation: " + this.cwd;
5✔
281
  }
282

283
  private String getMessageIdeRootNotFound() {
284

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

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

299
    return false;
×
300
  }
301

302
  protected SystemPath computeSystemPath() {
303

304
    return new SystemPath(this);
×
305
  }
306

307
  private boolean isIdeHome(Path dir) {
308

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

317
  private EnvironmentVariables createVariables() {
318

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

327
  protected AbstractEnvironmentVariables createSystemVariables() {
328

329
    return EnvironmentVariables.ofSystem(this);
3✔
330
  }
331

332
  @Override
333
  public SystemInfo getSystemInfo() {
334

335
    return this.systemInfo;
3✔
336
  }
337

338
  @Override
339
  public FileAccess getFileAccess() {
340

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

346
  @Override
347
  public CommandletManager getCommandletManager() {
348

349
    return this.commandletManager;
3✔
350
  }
351

352
  @Override
353
  public ToolRepository getDefaultToolRepository() {
354

355
    return this.defaultToolRepository;
3✔
356
  }
357

358
  @Override
359
  public CustomToolRepository getCustomToolRepository() {
360

361
    if (this.customToolRepository == null) {
3!
362
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
363
    }
364
    return this.customToolRepository;
3✔
365
  }
366

367
  @Override
368
  public Path getIdeHome() {
369

370
    return this.ideHome;
3✔
371
  }
372

373
  @Override
374
  public String getProjectName() {
375

376
    if (this.ideHome != null) {
3!
377
      return this.ideHome.getFileName().toString();
5✔
378
    }
379
    return "";
×
380
  }
381

382
  @Override
383
  public Path getIdeRoot() {
384

385
    return this.ideRoot;
3✔
386
  }
387

388
  @Override
389
  public Path getCwd() {
390

391
    return this.cwd;
3✔
392
  }
393

394
  @Override
395
  public Path getTempPath() {
396

397
    return this.tempPath;
3✔
398
  }
399

400
  @Override
401
  public Path getTempDownloadPath() {
402

403
    return this.tempDownloadPath;
3✔
404
  }
405

406
  @Override
407
  public Path getUserHome() {
408

409
    return this.userHome;
3✔
410
  }
411

412
  @Override
413
  public Path getUserHomeIde() {
414

415
    return this.userHomeIde;
3✔
416
  }
417

418
  @Override
419
  public Path getSettingsPath() {
420

421
    return this.settingsPath;
3✔
422
  }
423

424
  @Override
425
  public Path getSettingsGitRepository() {
426

427
    Path settingsPath = getSettingsPath();
3✔
428

429
    if (settingsPath == null) {
2✔
430
      error("No settings repository was found.");
3✔
431
      return null;
2✔
432
    }
433

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

440
    return settingsPath;
×
441
  }
442

443
  public boolean isSettingsRepositorySymlink() {
444

445
    Path settingsPath = getSettingsPath();
3✔
446
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
447
  }
448

449
  @Override
450
  public Path getSettingsCommitIdPath() {
451

452
    return this.settingsCommitIdPath;
3✔
453
  }
454

455
  @Override
456
  public Path getConfPath() {
457

458
    return this.confPath;
3✔
459
  }
460

461
  @Override
462
  public Path getSoftwarePath() {
463

464
    return this.softwarePath;
3✔
465
  }
466

467
  @Override
468
  public Path getSoftwareExtraPath() {
469

470
    return this.softwareExtraPath;
3✔
471
  }
472

473
  @Override
474
  public Path getSoftwareRepositoryPath() {
475

476
    return this.softwareRepositoryPath;
3✔
477
  }
478

479
  @Override
480
  public Path getPluginsPath() {
481

482
    return this.pluginsPath;
3✔
483
  }
484

485
  @Override
486
  public String getWorkspaceName() {
487

488
    return this.workspaceName;
3✔
489
  }
490

491
  @Override
492
  public Path getWorkspacePath() {
493

494
    return this.workspacePath;
3✔
495
  }
496

497
  @Override
498
  public Path getDownloadPath() {
499

500
    return this.downloadPath;
3✔
501
  }
502

503
  @Override
504
  public Path getUrlsPath() {
505

506
    return this.urlsPath;
3✔
507
  }
508

509
  @Override
510
  public Path getToolRepositoryPath() {
511

512
    return this.toolRepositoryPath;
3✔
513
  }
514

515
  @Override
516
  public SystemPath getPath() {
517

518
    return this.path;
3✔
519
  }
520

521
  @Override
522
  public EnvironmentVariables getVariables() {
523

524
    if (this.variables == null) {
3✔
525
      this.variables = createVariables();
4✔
526
    }
527
    return this.variables;
3✔
528
  }
529

530
  @Override
531
  public UrlMetadata getUrls() {
532

533
    if (this.urlMetadata == null) {
3✔
534
      if (!isTest()) {
3!
535
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, this.urlsPath, null);
×
536
      }
537
      this.urlMetadata = new UrlMetadata(this);
6✔
538
    }
539
    return this.urlMetadata;
3✔
540
  }
541

542
  @Override
543
  public boolean isQuietMode() {
544

545
    return this.startContext.isQuietMode();
4✔
546
  }
547

548
  @Override
549
  public boolean isBatchMode() {
550

551
    return this.startContext.isBatchMode();
×
552
  }
553

554
  @Override
555
  public boolean isForceMode() {
556

557
    return this.startContext.isForceMode();
4✔
558
  }
559

560
  @Override
561
  public boolean isOfflineMode() {
562

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

566
  @Override
567
  public boolean isSkipUpdatesMode() {
568

569
    return this.startContext.isSkipUpdatesMode();
×
570
  }
571

572
  @Override
573
  public boolean isOnline() {
574

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

594
  private void configureNetworkProxy() {
595

596
    if (this.networkProxy == null) {
3✔
597
      this.networkProxy = new NetworkProxy(this);
6✔
598
      this.networkProxy.configure();
3✔
599
    }
600
  }
1✔
601

602
  @Override
603
  public Locale getLocale() {
604

605
    Locale locale = this.startContext.getLocale();
4✔
606
    if (locale == null) {
2!
607
      locale = Locale.getDefault();
×
608
    }
609
    return locale;
2✔
610
  }
611

612
  @Override
613
  public DirectoryMerger getWorkspaceMerger() {
614

615
    if (this.workspaceMerger == null) {
3✔
616
      this.workspaceMerger = new DirectoryMerger(this);
6✔
617
    }
618
    return this.workspaceMerger;
3✔
619
  }
620

621
  /**
622
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is
623
   * executed.
624
   */
625
  @Override
626
  public Path getDefaultExecutionDirectory() {
627

628
    return this.defaultExecutionDirectory;
×
629
  }
630

631
  /**
632
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
633
   */
634
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
635

636
    if (defaultExecutionDirectory != null) {
×
637
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
638
    }
639
  }
×
640

641
  @Override
642
  public GitContext getGitContext() {
643

644
    return new GitContextImpl(this);
×
645
  }
646

647
  @Override
648
  public ProcessContext newProcess() {
649

650
    ProcessContext processContext = createProcessContext();
3✔
651
    if (this.defaultExecutionDirectory != null) {
3!
652
      processContext.directory(this.defaultExecutionDirectory);
×
653
    }
654
    return processContext;
2✔
655
  }
656

657
  @Override
658
  public IdeSystem getSystem() {
659

660
    if (this.system == null) {
×
661
      this.system = new IdeSystemImpl(this);
×
662
    }
663
    return this.system;
×
664
  }
665

666
  /**
667
   * @return a new instance of {@link ProcessContext}.
668
   * @see #newProcess()
669
   */
670
  protected ProcessContext createProcessContext() {
671

672
    return new ProcessContextImpl(this);
×
673
  }
674

675
  @Override
676
  public IdeSubLogger level(IdeLogLevel level) {
677

678
    return this.startContext.level(level);
5✔
679
  }
680

681
  @Override
682
  public void logIdeHomeAndRootStatus() {
683

684
    if (this.ideRoot != null) {
3!
685
      success("IDE_ROOT is set to {}", this.ideRoot);
×
686
    }
687
    if (this.ideHome == null) {
3!
688
      warning(getMessageIdeHomeNotFound());
5✔
689
    } else {
690
      success("IDE_HOME is set to {}", this.ideHome);
×
691
    }
692
  }
1✔
693

694
  @Override
695
  public String askForInput(String message, String defaultValue) {
696

697
    if (!message.isBlank()) {
×
698
      info(message);
×
699
    }
700
    if (isBatchMode()) {
×
701
      if (isForceMode()) {
×
702
        return defaultValue;
×
703
      } else {
704
        throw new CliAbortException();
×
705
      }
706
    }
707
    String input = readLine().trim();
×
708
    return input.isEmpty() ? defaultValue : input;
×
709
  }
710

711
  @Override
712
  public String askForInput(String message) {
713

714
    String input;
715
    do {
716
      info(message);
3✔
717
      input = readLine().trim();
4✔
718
    } while (input.isEmpty());
3!
719

720
    return input;
2✔
721
  }
722

723
  @SuppressWarnings("unchecked")
724
  @Override
725
  public <O> O question(String question, O... options) {
726

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

761
  /**
762
   * @return the input from the end-user (e.g. read from the console).
763
   */
764
  protected abstract String readLine();
765

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

768
    O duplicate = mapping.put(key, option);
×
769
    if (duplicate != null) {
×
770
      throw new IllegalArgumentException("Duplicated option " + key);
×
771
    }
772
  }
×
773

774
  @Override
775
  public Step getCurrentStep() {
776

777
    return this.currentStep;
×
778
  }
779

780
  @Override
781
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
782

783
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
784
    return this.currentStep;
3✔
785
  }
786

787
  /**
788
   * Internal method to end the running {@link Step}.
789
   *
790
   * @param step the current {@link Step} to end.
791
   */
792
  public void endStep(StepImpl step) {
793

794
    if (step == this.currentStep) {
4!
795
      this.currentStep = this.currentStep.getParent();
6✔
796
    } else {
797
      String currentStepName = "null";
×
798
      if (this.currentStep != null) {
×
799
        currentStepName = this.currentStep.getName();
×
800
      }
801
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
802
    }
803
  }
1✔
804

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

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

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

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

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

919
  private boolean ensureLicenseAgreement(Commandlet cmd) {
920

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

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

980
  private void verifyIdeRoot() {
981

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

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

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

1028
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1029

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

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

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

1141
  @Override
1142
  public String findBash() {
1143

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

1149
    return bash;
2✔
1150
  }
1151

1152
  private String findBashOnWindows() {
1153

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

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

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

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

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

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

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

1203
  @Override
1204
  public WindowsPathSyntax getPathSyntax() {
1205

1206
    return this.pathSyntax;
3✔
1207
  }
1208

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

1214
    this.pathSyntax = pathSyntax;
3✔
1215
  }
1✔
1216

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

1222
    return startContext;
3✔
1223
  }
1224

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

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