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

devonfw / IDEasy / 12987966179

27 Jan 2025 11:27AM UTC coverage: 68.326% (-0.1%) from 68.444%
12987966179

Pull #990

github

web-flow
Merge ca9b2b7a7 into c44479b1a
Pull Request #990: #954: improve repository support

2836 of 4561 branches covered (62.18%)

Branch coverage included in aggregate %.

7337 of 10328 relevant lines covered (71.04%)

3.09 hits per line

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

59.63
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.step.Step;
54
import com.devonfw.tools.ide.step.StepImpl;
55
import com.devonfw.tools.ide.tool.repository.CustomToolRepository;
56
import com.devonfw.tools.ide.tool.repository.CustomToolRepositoryImpl;
57
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
58
import com.devonfw.tools.ide.tool.repository.ToolRepository;
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 final Path ideInstallationPath;
82

83
  private Path confPath;
84

85
  protected Path settingsPath;
86

87
  private Path settingsCommitIdPath;
88

89
  private Path softwarePath;
90

91
  private Path softwareExtraPath;
92

93
  private final Path softwareRepositoryPath;
94

95
  protected Path pluginsPath;
96

97
  private Path workspacePath;
98

99
  private String workspaceName;
100

101
  protected Path urlsPath;
102

103
  private final Path tempPath;
104

105
  private final Path tempDownloadPath;
106

107
  private Path cwd;
108

109
  private Path downloadPath;
110

111
  private final Path toolRepositoryPath;
112

113
  protected Path userHome;
114

115
  private Path userHomeIde;
116

117
  private SystemPath path;
118

119
  private WindowsPathSyntax pathSyntax;
120

121
  private final SystemInfo systemInfo;
122

123
  private EnvironmentVariables variables;
124

125
  private final FileAccess fileAccess;
126

127
  protected CommandletManager commandletManager;
128

129
  protected ToolRepository defaultToolRepository;
130

131
  private CustomToolRepository customToolRepository;
132

133
  private DirectoryMerger workspaceMerger;
134

135
  protected UrlMetadata urlMetadata;
136

137
  protected Path defaultExecutionDirectory;
138

139
  private StepImpl currentStep;
140

141
  protected Boolean online;
142

143
  protected IdeSystem system;
144

145
  private NetworkProxy networkProxy;
146

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

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

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

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

191
    if (this.ideRoot == null) {
3✔
192
      this.ideInstallationPath = 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
      this.ideInstallationPath = this.ideRoot.resolve(FOLDER_IDE_INSTALLATION);
6✔
200
      this.toolRepositoryPath = this.ideInstallationPath.resolve("software");
6✔
201
      this.urlsPath = this.ideInstallationPath.resolve("urls");
6✔
202
      this.tempPath = this.ideInstallationPath.resolve("tmp");
6✔
203
      this.tempDownloadPath = this.tempPath.resolve(FOLDER_DOWNLOADS);
6✔
204
      this.softwareRepositoryPath = this.ideInstallationPath.resolve(FOLDER_SOFTWARE);
6✔
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
  }
1✔
214

215
  private Path findIdeRoot(Path ideHomePath) {
216

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

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

237
  @Override
238
  public void setCwd(Path userDir, String workspace, Path ideHome) {
239

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

272
    this.path = computeSystemPath();
4✔
273
  }
1✔
274

275
  private String getMessageIdeHomeFound() {
276

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

280
  private String getMessageIdeHomeNotFound() {
281

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

285
  private String getMessageIdeRootNotFound() {
286
    String root = getSystem().getEnv("IDE_ROOT");
5✔
287
    if (root == null) {
2!
288
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
289
    } else {
290
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". 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

308
  private boolean isIdeHome(Path dir) {
309

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

318
  private EnvironmentVariables createVariables() {
319

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

328
  protected AbstractEnvironmentVariables createSystemVariables() {
329

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

333
  @Override
334
  public SystemInfo getSystemInfo() {
335

336
    return this.systemInfo;
3✔
337
  }
338

339
  @Override
340
  public FileAccess getFileAccess() {
341

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

347
  @Override
348
  public CommandletManager getCommandletManager() {
349

350
    return this.commandletManager;
3✔
351
  }
352

353
  @Override
354
  public ToolRepository getDefaultToolRepository() {
355

356
    return this.defaultToolRepository;
3✔
357
  }
358

359
  @Override
360
  public CustomToolRepository getCustomToolRepository() {
361

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

368
  @Override
369
  public Path getIdeHome() {
370

371
    return this.ideHome;
3✔
372
  }
373

374
  @Override
375
  public String getProjectName() {
376

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

383
  @Override
384
  public Path getIdeRoot() {
385

386
    return this.ideRoot;
3✔
387
  }
388

389
  @Override
390
  public Path getIdeInstallationPath() {
391

392
    return this.ideInstallationPath;
×
393
  }
394

395
  @Override
396
  public Path getCwd() {
397

398
    return this.cwd;
3✔
399
  }
400

401
  @Override
402
  public Path getTempPath() {
403

404
    return this.tempPath;
3✔
405
  }
406

407
  @Override
408
  public Path getTempDownloadPath() {
409

410
    return this.tempDownloadPath;
3✔
411
  }
412

413
  @Override
414
  public Path getUserHome() {
415

416
    return this.userHome;
3✔
417
  }
418

419
  @Override
420
  public Path getUserHomeIde() {
421

422
    return this.userHomeIde;
3✔
423
  }
424

425
  @Override
426
  public Path getSettingsPath() {
427

428
    return this.settingsPath;
3✔
429
  }
430

431
  @Override
432
  public Path getSettingsGitRepository() {
433

434
    Path settingsPath = getSettingsPath();
3✔
435

436
    if (settingsPath == null) {
2✔
437
      error("No settings repository was found.");
3✔
438
      return null;
2✔
439
    }
440

441
    // check whether the settings path has a .git folder only if its not a symbolic link
442
    if (!Files.exists(settingsPath.resolve(".git")) && !Files.isSymbolicLink(settingsPath)) {
10!
443
      error("Settings repository exists but is not a git repository.");
3✔
444
      return null;
2✔
445
    }
446

447
    return settingsPath;
×
448
  }
449

450
  @Override
451
  public Path getSettingsCommitIdPath() {
452

453
    return this.settingsCommitIdPath;
3✔
454
  }
455

456
  @Override
457
  public Path getConfPath() {
458

459
    return this.confPath;
3✔
460
  }
461

462
  @Override
463
  public Path getSoftwarePath() {
464

465
    return this.softwarePath;
3✔
466
  }
467

468
  @Override
469
  public Path getSoftwareExtraPath() {
470

471
    return this.softwareExtraPath;
3✔
472
  }
473

474
  @Override
475
  public Path getSoftwareRepositoryPath() {
476

477
    return this.softwareRepositoryPath;
3✔
478
  }
479

480
  @Override
481
  public Path getPluginsPath() {
482

483
    return this.pluginsPath;
3✔
484
  }
485

486
  @Override
487
  public String getWorkspaceName() {
488

489
    return this.workspaceName;
3✔
490
  }
491

492
  @Override
493
  public Path getWorkspacePath() {
494

495
    return this.workspacePath;
3✔
496
  }
497

498
  @Override
499
  public Path getDownloadPath() {
500

501
    return this.downloadPath;
3✔
502
  }
503

504
  @Override
505
  public Path getUrlsPath() {
506

507
    return this.urlsPath;
3✔
508
  }
509

510
  @Override
511
  public Path getToolRepositoryPath() {
512

513
    return this.toolRepositoryPath;
3✔
514
  }
515

516
  @Override
517
  public SystemPath getPath() {
518

519
    return this.path;
3✔
520
  }
521

522
  @Override
523
  public EnvironmentVariables getVariables() {
524

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

531
  @Override
532
  public UrlMetadata getUrls() {
533

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

543
  @Override
544
  public boolean isQuietMode() {
545

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

549
  @Override
550
  public boolean isBatchMode() {
551

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

555
  @Override
556
  public boolean isForceMode() {
557

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

561
  @Override
562
  public boolean isOfflineMode() {
563

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

567
  @Override
568
  public boolean isSkipUpdatesMode() {
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
    if (this.networkProxy == null) {
3✔
596
      this.networkProxy = new NetworkProxy(this);
6✔
597
      this.networkProxy.configure();
3✔
598
    }
599
  }
1✔
600

601
  @Override
602
  public Locale getLocale() {
603

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

611
  @Override
612
  public DirectoryMerger getWorkspaceMerger() {
613

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

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

626
    return this.defaultExecutionDirectory;
×
627
  }
628

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

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

639
  @Override
640
  public GitContext getGitContext() {
641

642
    return new GitContextImpl(this);
×
643
  }
644

645
  @Override
646
  public ProcessContext newProcess() {
647

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

655
  @Override
656
  public IdeSystem getSystem() {
657

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

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

670
    return new ProcessContextImpl(this);
×
671
  }
672

673
  @Override
674
  public IdeSubLogger level(IdeLogLevel level) {
675

676
    return this.startContext.level(level);
5✔
677
  }
678

679
  @Override
680
  public void logIdeHomeAndRootStatus() {
681

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

692
  @Override
693
  public String askForInput(String message, String defaultValue) {
694

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

709
  @Override
710
  public String askForInput(String message) {
711

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

718
    return input;
2✔
719
  }
720

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

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

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

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

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

772
  @Override
773
  public Step getCurrentStep() {
774

775
    return this.currentStep;
×
776
  }
777

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

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

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

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

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

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

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

857
    IdeLogLevel previousLogLevel = null;
2✔
858
    cmd.reset();
2✔
859
    ValidationResult result = apply(arguments, cmd);
5✔
860
    if (result.isValid()) {
3!
861
      result = cmd.validate();
3✔
862
    }
863
    if (result.isValid()) {
3!
864
      debug("Running commandlet {}", cmd);
9✔
865
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
866
        throw new CliException(getMessageIdeHomeNotFound(), ProcessResult.NO_IDE_HOME);
×
867
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6✔
868
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
869
      }
870
      try {
871
        if (cmd.isProcessableOutput()) {
3!
872
          if (!debug().isEnabled()) {
×
873
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
874
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
875
          }
876
          this.startContext.activateLogging();
×
877
        } else {
878
          this.startContext.activateLogging();
3✔
879
          verifyIdeRoot();
2✔
880
          if (cmd.isIdeHomeRequired()) {
3✔
881
            debug(getMessageIdeHomeFound());
4✔
882
          }
883
          Path settingsRepository = getSettingsGitRepository();
3✔
884
          if (settingsRepository != null) {
2!
885
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) ||
×
886
                (getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(settingsRepository,
×
887
                    getSettingsCommitIdPath()))) {
×
888
              interaction("Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
×
889
            }
890
          }
891
        }
892
        boolean success = ensureLicenseAgreement(cmd);
4✔
893
        if (!success) {
2!
894
          return ValidationResultValid.get();
×
895
        }
896
        cmd.run();
2✔
897
      } finally {
898
        if (previousLogLevel != null) {
2!
899
          this.startContext.setLogLevel(previousLogLevel);
×
900
        }
901
      }
1✔
902
    } else {
903
      trace("Commandlet did not match");
×
904
    }
905
    return result;
2✔
906
  }
907

908
  private boolean ensureLicenseAgreement(Commandlet cmd) {
909

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

951
    sb.setLength(0);
×
952
    LocalDateTime now = LocalDateTime.now();
×
953
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
954
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
955
    try {
956
      Files.writeString(licenseAgreement, sb);
×
957
    } catch (Exception e) {
×
958
      throw new RuntimeException("Failed to save license agreement!", e);
×
959
    }
×
960
    if (logLevelInfoDisabled) {
×
961
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
962
    }
963
    if (logLevelInteractionDisabled) {
×
964
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
965
    }
966
    return true;
×
967
  }
968

969
  private void verifyIdeRoot() {
970
    if (!isTest()) {
3!
971
      if (this.ideRoot == null) {
×
972
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
973
      } else if (this.ideHome != null) {
×
974
        Path ideRootPath = getIdeRootPathFromEnv();
×
975
        if (!this.ideRoot.equals(ideRootPath)) {
×
976
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath,
×
977
              this.ideHome.getFileName(), this.ideRoot);
×
978
        }
979
      }
980
    }
981
  }
1✔
982

983
  /**
984
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
985
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
986
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
987
   */
988
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
989
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
990
    if (arguments.current().isStart()) {
4✔
991
      arguments.next();
3✔
992
    }
993
    if (includeContextOptions) {
2✔
994
      ContextCommandlet cc = new ContextCommandlet();
4✔
995
      for (Property<?> property : cc.getProperties()) {
11✔
996
        assert (property.isOption());
4!
997
        property.apply(arguments, this, cc, collector);
7✔
998
      }
1✔
999
    }
1000
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1001
    CliArgument current = arguments.current();
3✔
1002
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1003
      collector.add(current.get(), null, null, null);
7✔
1004
    }
1005
    arguments.next();
3✔
1006
    while (commandletIterator.hasNext()) {
3✔
1007
      Commandlet cmd = commandletIterator.next();
4✔
1008
      if (!arguments.current().isEnd()) {
4✔
1009
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1010
      }
1011
    }
1✔
1012
    return collector.getSortedCandidates();
3✔
1013
  }
1014

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

1072

1073
  /**
1074
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1075
   *     {@link CliArguments#copy() copy} as needed.
1076
   * @param cmd the potential {@link Commandlet} to match.
1077
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1078
   */
1079
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1080

1081
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1082
    CliArgument currentArgument = arguments.current();
3✔
1083
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1084
    Property<?> property = null;
2✔
1085
    if (propertyIterator.hasNext()) {
3!
1086
      property = propertyIterator.next();
4✔
1087
    }
1088
    while (!currentArgument.isEnd()) {
3✔
1089
      trace("Trying to match argument '{}'", currentArgument);
9✔
1090
      Property<?> currentProperty = property;
2✔
1091
      if (!arguments.isEndOptions()) {
3!
1092
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1093
        if (option != null) {
2!
1094
          currentProperty = option;
×
1095
        }
1096
      }
1097
      if (currentProperty == null) {
2!
1098
        trace("No option or next value found");
×
1099
        ValidationState state = new ValidationState(null);
×
1100
        state.addErrorMessage("No matching property found");
×
1101
        return state;
×
1102
      }
1103
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1104
      if (currentProperty == property) {
3!
1105
        if (!property.isMultiValued()) {
3✔
1106
          if (propertyIterator.hasNext()) {
3✔
1107
            property = propertyIterator.next();
5✔
1108
          } else {
1109
            property = null;
2✔
1110
          }
1111
        }
1112
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1113
          arguments.stopSplitShortOptions();
2✔
1114
        }
1115
      }
1116
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1117
      if (!matches && currentArgument.isCompletion()) {
2!
1118
        ValidationState state = new ValidationState(null);
×
1119
        state.addErrorMessage("No matching property found");
×
1120
        return state;
×
1121
      }
1122
      currentArgument = arguments.current();
3✔
1123
    }
1✔
1124
    return ValidationResultValid.get();
2✔
1125
  }
1126

1127
  @Override
1128
  public String findBash() {
1129

1130
    String bash = "bash";
2✔
1131
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1132
      bash = findBashOnWindows();
×
1133
    }
1134

1135
    return bash;
2✔
1136
  }
1137

1138
  private String findBashOnWindows() {
1139

1140
    // Check if Git Bash exists in the default location
1141
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1142
    if (Files.exists(defaultPath)) {
×
1143
      return defaultPath.toString();
×
1144
    }
1145

1146
    // If not found in the default location, try the registry query
1147
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1148
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1149
    String regQueryResult;
1150
    for (String bashVariant : bashVariants) {
×
1151
      for (String registryKey : registryKeys) {
×
1152
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1153
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1154

1155
        try {
1156
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1157
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1158
            StringBuilder output = new StringBuilder();
×
1159
            String line;
1160

1161
            while ((line = reader.readLine()) != null) {
×
1162
              output.append(line);
×
1163
            }
1164

1165
            int exitCode = process.waitFor();
×
1166
            if (exitCode != 0) {
×
1167
              return null;
×
1168
            }
1169

1170
            regQueryResult = output.toString();
×
1171
            if (regQueryResult != null) {
×
1172
              int index = regQueryResult.indexOf("REG_SZ");
×
1173
              if (index != -1) {
×
1174
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1175
                return path + "\\bin\\bash.exe";
×
1176
              }
1177
            }
1178

1179
          }
×
1180
        } catch (Exception e) {
×
1181
          return null;
×
1182
        }
×
1183
      }
1184
    }
1185
    // no bash found
1186
    return null;
×
1187
  }
1188

1189
  @Override
1190
  public WindowsPathSyntax getPathSyntax() {
1191
    return this.pathSyntax;
3✔
1192
  }
1193

1194
  /**
1195
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1196
   */
1197
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1198

1199
    this.pathSyntax = pathSyntax;
3✔
1200
  }
1✔
1201

1202
  /**
1203
   * @return the {@link IdeStartContextImpl}.
1204
   */
1205
  public IdeStartContextImpl getStartContext() {
1206

1207
    return startContext;
3✔
1208
  }
1209

1210
  /**
1211
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1212
   */
1213
  public void reload() {
1214
    this.variables = null;
3✔
1215
    this.customToolRepository = null;
3✔
1216
  }
1✔
1217
}
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