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

devonfw / IDEasy / 12876082916

20 Jan 2025 09:43PM UTC coverage: 68.126% (-0.2%) from 68.372%
12876082916

push

github

web-flow
#919: Require user to agree to license (#948)

2739 of 4397 branches covered (62.29%)

Branch coverage included in aggregate %.

7080 of 10016 relevant lines covered (70.69%)

3.08 hits per line

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

59.25
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
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
225
    if (root != null) {
×
226
      Path rootPath = Path.of(root);
×
227
      if (Files.isDirectory(rootPath)) {
×
228
        return rootPath;
×
229
      }
230
    }
231
    return null;
×
232
  }
233

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

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

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

272
  private String getMessageIdeHomeFound() {
273

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

277
  private String getMessageIdeHomeNotFound() {
278

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

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

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

296
    return false;
×
297
  }
298

299
  protected SystemPath computeSystemPath() {
300

301
    return new SystemPath(this);
×
302
  }
303

304

305
  private boolean isIdeHome(Path dir) {
306

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

315
  private EnvironmentVariables createVariables() {
316

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

325
  protected AbstractEnvironmentVariables createSystemVariables() {
326

327
    return EnvironmentVariables.ofSystem(this);
3✔
328
  }
329

330
  @Override
331
  public SystemInfo getSystemInfo() {
332

333
    return this.systemInfo;
3✔
334
  }
335

336
  @Override
337
  public FileAccess getFileAccess() {
338

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

344
  @Override
345
  public CommandletManager getCommandletManager() {
346

347
    return this.commandletManager;
3✔
348
  }
349

350
  @Override
351
  public ToolRepository getDefaultToolRepository() {
352

353
    return this.defaultToolRepository;
3✔
354
  }
355

356
  @Override
357
  public CustomToolRepository getCustomToolRepository() {
358

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

365
  @Override
366
  public Path getIdeHome() {
367

368
    return this.ideHome;
3✔
369
  }
370

371
  @Override
372
  public String getProjectName() {
373

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

380
  @Override
381
  public Path getIdeRoot() {
382

383
    return this.ideRoot;
3✔
384
  }
385

386
  @Override
387
  public Path getCwd() {
388

389
    return this.cwd;
3✔
390
  }
391

392
  @Override
393
  public Path getTempPath() {
394

395
    return this.tempPath;
3✔
396
  }
397

398
  @Override
399
  public Path getTempDownloadPath() {
400

401
    return this.tempDownloadPath;
3✔
402
  }
403

404
  @Override
405
  public Path getUserHome() {
406

407
    return this.userHome;
3✔
408
  }
409

410
  @Override
411
  public Path getUserHomeIde() {
412

413
    return this.userHomeIde;
3✔
414
  }
415

416
  @Override
417
  public Path getSettingsPath() {
418

419
    return this.settingsPath;
3✔
420
  }
421

422
  @Override
423
  public Path getSettingsGitRepository() {
424

425
    Path settingsPath = getSettingsPath();
3✔
426

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

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

438
    return settingsPath;
×
439
  }
440

441
  @Override
442
  public Path getSettingsCommitIdPath() {
443

444
    return this.settingsCommitIdPath;
3✔
445
  }
446

447
  @Override
448
  public Path getConfPath() {
449

450
    return this.confPath;
3✔
451
  }
452

453
  @Override
454
  public Path getSoftwarePath() {
455

456
    return this.softwarePath;
3✔
457
  }
458

459
  @Override
460
  public Path getSoftwareExtraPath() {
461

462
    return this.softwareExtraPath;
3✔
463
  }
464

465
  @Override
466
  public Path getSoftwareRepositoryPath() {
467

468
    return this.softwareRepositoryPath;
3✔
469
  }
470

471
  @Override
472
  public Path getPluginsPath() {
473

474
    return this.pluginsPath;
3✔
475
  }
476

477
  @Override
478
  public String getWorkspaceName() {
479

480
    return this.workspaceName;
3✔
481
  }
482

483
  @Override
484
  public Path getWorkspacePath() {
485

486
    return this.workspacePath;
3✔
487
  }
488

489
  @Override
490
  public Path getDownloadPath() {
491

492
    return this.downloadPath;
3✔
493
  }
494

495
  @Override
496
  public Path getUrlsPath() {
497

498
    return this.urlsPath;
3✔
499
  }
500

501
  @Override
502
  public Path getToolRepositoryPath() {
503

504
    return this.toolRepositoryPath;
3✔
505
  }
506

507
  @Override
508
  public SystemPath getPath() {
509

510
    return this.path;
3✔
511
  }
512

513
  @Override
514
  public EnvironmentVariables getVariables() {
515

516
    if (this.variables == null) {
3✔
517
      this.variables = createVariables();
4✔
518
    }
519
    return this.variables;
3✔
520
  }
521

522
  @Override
523
  public UrlMetadata getUrls() {
524

525
    if (this.urlMetadata == null) {
3✔
526
      if (!isTest()) {
3!
527
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, this.urlsPath, null);
×
528
      }
529
      this.urlMetadata = new UrlMetadata(this);
6✔
530
    }
531
    return this.urlMetadata;
3✔
532
  }
533

534
  @Override
535
  public boolean isQuietMode() {
536

537
    return this.startContext.isQuietMode();
4✔
538
  }
539

540
  @Override
541
  public boolean isBatchMode() {
542

543
    return this.startContext.isBatchMode();
×
544
  }
545

546
  @Override
547
  public boolean isForceMode() {
548

549
    return this.startContext.isForceMode();
4✔
550
  }
551

552
  @Override
553
  public boolean isOfflineMode() {
554

555
    return this.startContext.isOfflineMode();
4✔
556
  }
557

558
  @Override
559
  public boolean isSkipUpdatesMode() {
560
    return this.startContext.isSkipUpdatesMode();
×
561
  }
562

563
  @Override
564
  public boolean isOnline() {
565

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

585
  private void configureNetworkProxy() {
586
    if (this.networkProxy == null) {
3✔
587
      this.networkProxy = new NetworkProxy(this);
6✔
588
      this.networkProxy.configure();
3✔
589
    }
590
  }
1✔
591

592
  @Override
593
  public Locale getLocale() {
594

595
    Locale locale = this.startContext.getLocale();
4✔
596
    if (locale == null) {
2!
597
      locale = Locale.getDefault();
×
598
    }
599
    return locale;
2✔
600
  }
601

602
  @Override
603
  public DirectoryMerger getWorkspaceMerger() {
604

605
    if (this.workspaceMerger == null) {
3✔
606
      this.workspaceMerger = new DirectoryMerger(this);
6✔
607
    }
608
    return this.workspaceMerger;
3✔
609
  }
610

611
  /**
612
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
613
   */
614
  @Override
615
  public Path getDefaultExecutionDirectory() {
616

617
    return this.defaultExecutionDirectory;
×
618
  }
619

620
  /**
621
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
622
   */
623
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
624

625
    if (defaultExecutionDirectory != null) {
×
626
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
627
    }
628
  }
×
629

630
  @Override
631
  public GitContext getGitContext() {
632

633
    return new GitContextImpl(this);
×
634
  }
635

636
  @Override
637
  public ProcessContext newProcess() {
638

639
    ProcessContext processContext = createProcessContext();
3✔
640
    if (this.defaultExecutionDirectory != null) {
3!
641
      processContext.directory(this.defaultExecutionDirectory);
×
642
    }
643
    return processContext;
2✔
644
  }
645

646
  @Override
647
  public IdeSystem getSystem() {
648

649
    if (this.system == null) {
×
650
      this.system = new IdeSystemImpl(this);
×
651
    }
652
    return this.system;
×
653
  }
654

655
  /**
656
   * @return a new instance of {@link ProcessContext}.
657
   * @see #newProcess()
658
   */
659
  protected ProcessContext createProcessContext() {
660

661
    return new ProcessContextImpl(this);
×
662
  }
663

664
  @Override
665
  public IdeSubLogger level(IdeLogLevel level) {
666

667
    return this.startContext.level(level);
5✔
668
  }
669

670
  @Override
671
  public void logIdeHomeAndRootStatus() {
672

673
    if (this.ideRoot != null) {
3!
674
      success("IDE_ROOT is set to {}", this.ideRoot);
×
675
    }
676
    if (this.ideHome == null) {
3!
677
      warning(getMessageIdeHomeNotFound());
5✔
678
    } else {
679
      success("IDE_HOME is set to {}", this.ideHome);
×
680
    }
681
  }
1✔
682

683
  @Override
684
  public String askForInput(String message, String defaultValue) {
685

686
    if (!message.isBlank()) {
×
687
      info(message);
×
688
    }
689
    if (isBatchMode()) {
×
690
      if (isForceMode()) {
×
691
        return defaultValue;
×
692
      } else {
693
        throw new CliAbortException();
×
694
      }
695
    }
696
    String input = readLine().trim();
×
697
    return input.isEmpty() ? defaultValue : input;
×
698
  }
699

700
  @Override
701
  public String askForInput(String message) {
702

703
    String input;
704
    do {
705
      info(message);
3✔
706
      input = readLine().trim();
4✔
707
    } while (input.isEmpty());
3!
708

709
    return input;
2✔
710
  }
711

712
  @SuppressWarnings("unchecked")
713
  @Override
714
  public <O> O question(String question, O... options) {
715

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

750
  /**
751
   * @return the input from the end-user (e.g. read from the console).
752
   */
753
  protected abstract String readLine();
754

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

757
    O duplicate = mapping.put(key, option);
×
758
    if (duplicate != null) {
×
759
      throw new IllegalArgumentException("Duplicated option " + key);
×
760
    }
761
  }
×
762

763
  @Override
764
  public Step getCurrentStep() {
765

766
    return this.currentStep;
×
767
  }
768

769
  @Override
770
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
771

772
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
773
    return this.currentStep;
3✔
774
  }
775

776
  /**
777
   * Internal method to end the running {@link Step}.
778
   *
779
   * @param step the current {@link Step} to end.
780
   */
781
  public void endStep(StepImpl step) {
782

783
    if (step == this.currentStep) {
4!
784
      this.currentStep = this.currentStep.getParent();
6✔
785
    } else {
786
      String currentStepName = "null";
×
787
      if (this.currentStep != null) {
×
788
        currentStepName = this.currentStep.getName();
×
789
      }
790
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
791
    }
792
  }
1✔
793

794
  /**
795
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
796
   *
797
   * @param arguments the {@link CliArgument}.
798
   * @return the return code of the execution.
799
   */
800
  public int run(CliArguments arguments) {
801

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

841
  /**
842
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
843
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
844
   *     {@link Commandlet} did not match and we have to try a different candidate).
845
   */
846
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
847

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

899
  private boolean ensureLicenseAgreement(Commandlet cmd) {
900

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

942
    sb.setLength(0);
×
943
    LocalDateTime now = LocalDateTime.now();
×
944
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
945
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
946
    try {
947
      Files.writeString(licenseAgreement, sb);
×
948
    } catch (Exception e) {
×
949
      throw new RuntimeException("Failed to save license agreement!", e);
×
950
    }
×
951
    if (logLevelInfoDisabled) {
×
952
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
953
    }
954
    if (logLevelInteractionDisabled) {
×
955
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
956
    }
957
    return true;
×
958
  }
959

960
  private void verifyIdeRoot() {
961
    if (!isTest()) {
3!
962
      if (this.ideRoot == null) {
×
963
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
964
      } else if (this.ideHome != null) {
×
965
        Path ideRootPath = getIdeRootPathFromEnv();
×
966
        if (!this.ideRoot.equals(ideRootPath)) {
×
967
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath,
×
968
              this.ideHome.getFileName(), this.ideRoot);
×
969
        }
970
      }
971
    }
972
  }
1✔
973

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

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

1063

1064
  /**
1065
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1066
   *     {@link CliArguments#copy() copy} as needed.
1067
   * @param cmd the potential {@link Commandlet} to match.
1068
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1069
   */
1070
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1071

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

1118
  @Override
1119
  public String findBash() {
1120

1121
    String bash = "bash";
2✔
1122
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1123
      bash = findBashOnWindows();
×
1124
    }
1125

1126
    return bash;
2✔
1127
  }
1128

1129
  private String findBashOnWindows() {
1130

1131
    // Check if Git Bash exists in the default location
1132
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1133
    if (Files.exists(defaultPath)) {
×
1134
      return defaultPath.toString();
×
1135
    }
1136

1137
    // If not found in the default location, try the registry query
1138
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1139
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1140
    String regQueryResult;
1141
    for (String bashVariant : bashVariants) {
×
1142
      for (String registryKey : registryKeys) {
×
1143
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1144
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1145

1146
        try {
1147
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1148
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1149
            StringBuilder output = new StringBuilder();
×
1150
            String line;
1151

1152
            while ((line = reader.readLine()) != null) {
×
1153
              output.append(line);
×
1154
            }
1155

1156
            int exitCode = process.waitFor();
×
1157
            if (exitCode != 0) {
×
1158
              return null;
×
1159
            }
1160

1161
            regQueryResult = output.toString();
×
1162
            if (regQueryResult != null) {
×
1163
              int index = regQueryResult.indexOf("REG_SZ");
×
1164
              if (index != -1) {
×
1165
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1166
                return path + "\\bin\\bash.exe";
×
1167
              }
1168
            }
1169

1170
          }
×
1171
        } catch (Exception e) {
×
1172
          return null;
×
1173
        }
×
1174
      }
1175
    }
1176
    // no bash found
1177
    return null;
×
1178
  }
1179

1180
  @Override
1181
  public WindowsPathSyntax getPathSyntax() {
1182
    return this.pathSyntax;
3✔
1183
  }
1184

1185
  /**
1186
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1187
   */
1188
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1189

1190
    this.pathSyntax = pathSyntax;
3✔
1191
  }
1✔
1192

1193
  /**
1194
   * @return the {@link IdeStartContextImpl}.
1195
   */
1196
  public IdeStartContextImpl getStartContext() {
1197

1198
    return startContext;
3✔
1199
  }
1200

1201
  /**
1202
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1203
   */
1204
  public void reload() {
1205
    this.variables = null;
3✔
1206
    this.customToolRepository = null;
3✔
1207
  }
1✔
1208
}
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