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

devonfw / IDEasy / 13062819017

30 Jan 2025 10:58PM UTC coverage: 68.293% (-0.3%) from 68.557%
13062819017

Pull #1002

github

web-flow
Merge e11b708f0 into aaf4eb4e0
Pull Request #1002: #786: Upgrade commandlet

2864 of 4597 branches covered (62.3%)

Branch coverage included in aggregate %.

7399 of 10431 relevant lines covered (70.93%)

3.09 hits per line

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

59.32
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.MavenRepository;
57
import com.devonfw.tools.ide.repo.ToolRepository;
58
import com.devonfw.tools.ide.step.Step;
59
import com.devonfw.tools.ide.step.StepImpl;
60
import com.devonfw.tools.ide.url.model.UrlMetadata;
61
import com.devonfw.tools.ide.util.DateTimeUtil;
62
import com.devonfw.tools.ide.validation.ValidationResult;
63
import com.devonfw.tools.ide.validation.ValidationResultValid;
64
import com.devonfw.tools.ide.validation.ValidationState;
65
import com.devonfw.tools.ide.variable.IdeVariables;
66

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

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

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

76
  private final IdeStartContextImpl startContext;
77

78
  private Path ideHome;
79

80
  private final Path ideRoot;
81

82
  private Path confPath;
83

84
  protected Path settingsPath;
85

86
  private Path settingsCommitIdPath;
87

88
  private Path softwarePath;
89

90
  private Path softwareExtraPath;
91

92
  private final Path softwareRepositoryPath;
93

94
  protected Path pluginsPath;
95

96
  private Path workspacePath;
97

98
  private String workspaceName;
99

100
  protected Path urlsPath;
101

102
  private final Path tempPath;
103

104
  private final Path tempDownloadPath;
105

106
  private Path cwd;
107

108
  private Path downloadPath;
109

110
  private final Path toolRepositoryPath;
111

112
  protected Path userHome;
113

114
  private Path userHomeIde;
115

116
  private SystemPath path;
117

118
  private WindowsPathSyntax pathSyntax;
119

120
  private final SystemInfo systemInfo;
121

122
  private EnvironmentVariables variables;
123

124
  private final FileAccess fileAccess;
125

126
  protected CommandletManager commandletManager;
127

128
  protected ToolRepository defaultToolRepository;
129

130
  private CustomToolRepository customToolRepository;
131

132
  private MavenRepository mavenRepository;
133

134
  private DirectoryMerger workspaceMerger;
135

136
  protected UrlMetadata urlMetadata;
137

138
  protected Path defaultExecutionDirectory;
139

140
  private StepImpl currentStep;
141

142
  protected Boolean online;
143

144
  protected IdeSystem system;
145

146
  private NetworkProxy networkProxy;
147

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

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

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

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

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

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

216
  private Path findIdeRoot(Path ideHomePath) {
217

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

227
  private Path getIdeRootPathFromEnv() {
228

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

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

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

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

277
  private String getMessageIdeHomeFound() {
278

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

282
  private String getMessageIdeHomeNotFound() {
283

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

287
  private String getMessageIdeRootNotFound() {
288

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

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

302
    return false;
×
303
  }
304

305
  protected SystemPath computeSystemPath() {
306

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

310
  private boolean isIdeHome(Path dir) {
311

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

320
  private EnvironmentVariables createVariables() {
321

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

330
  protected AbstractEnvironmentVariables createSystemVariables() {
331

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

335
  @Override
336
  public SystemInfo getSystemInfo() {
337

338
    return this.systemInfo;
3✔
339
  }
340

341
  @Override
342
  public FileAccess getFileAccess() {
343

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

349
  @Override
350
  public CommandletManager getCommandletManager() {
351

352
    return this.commandletManager;
3✔
353
  }
354

355
  @Override
356
  public ToolRepository getDefaultToolRepository() {
357

358
    return this.defaultToolRepository;
3✔
359
  }
360

361
  @Override
362
  public MavenRepository getMavenSoftwareRepository() {
363

364
    return this.mavenRepository;
×
365
  }
366

367
  @Override
368
  public CustomToolRepository getCustomToolRepository() {
369

370
    if (this.customToolRepository == null) {
3!
371
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
372
    }
373
    return this.customToolRepository;
3✔
374
  }
375

376
  @Override
377
  public Path getIdeHome() {
378

379
    return this.ideHome;
3✔
380
  }
381

382
  @Override
383
  public String getProjectName() {
384

385
    if (this.ideHome != null) {
3!
386
      return this.ideHome.getFileName().toString();
5✔
387
    }
388
    return "";
×
389
  }
390

391
  @Override
392
  public Path getIdeRoot() {
393

394
    return this.ideRoot;
3✔
395
  }
396

397
  @Override
398
  public Path getCwd() {
399

400
    return this.cwd;
3✔
401
  }
402

403
  @Override
404
  public Path getTempPath() {
405

406
    return this.tempPath;
3✔
407
  }
408

409
  @Override
410
  public Path getTempDownloadPath() {
411

412
    return this.tempDownloadPath;
3✔
413
  }
414

415
  @Override
416
  public Path getUserHome() {
417

418
    return this.userHome;
3✔
419
  }
420

421
  @Override
422
  public Path getUserHomeIde() {
423

424
    return this.userHomeIde;
3✔
425
  }
426

427
  @Override
428
  public Path getSettingsPath() {
429

430
    return this.settingsPath;
3✔
431
  }
432

433
  @Override
434
  public Path getSettingsGitRepository() {
435

436
    Path settingsPath = getSettingsPath();
3✔
437

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

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

449
    return settingsPath;
×
450
  }
451

452
  @Override
453
  public boolean isSettingsRepositorySymlinkOrJunction() {
454

455
    Path settingsPath = getSettingsPath();
3✔
456
    if (settingsPath == null) {
2!
457
      return false;
×
458
    }
459
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
460
  }
461

462
  @Override
463
  public Path getSettingsCommitIdPath() {
464

465
    return this.settingsCommitIdPath;
3✔
466
  }
467

468
  @Override
469
  public Path getConfPath() {
470

471
    return this.confPath;
3✔
472
  }
473

474
  @Override
475
  public Path getSoftwarePath() {
476

477
    return this.softwarePath;
3✔
478
  }
479

480
  @Override
481
  public Path getSoftwareExtraPath() {
482

483
    return this.softwareExtraPath;
3✔
484
  }
485

486
  @Override
487
  public Path getSoftwareRepositoryPath() {
488

489
    return this.softwareRepositoryPath;
3✔
490
  }
491

492
  @Override
493
  public Path getPluginsPath() {
494

495
    return this.pluginsPath;
3✔
496
  }
497

498
  @Override
499
  public String getWorkspaceName() {
500

501
    return this.workspaceName;
3✔
502
  }
503

504
  @Override
505
  public Path getWorkspacePath() {
506

507
    return this.workspacePath;
3✔
508
  }
509

510
  @Override
511
  public Path getDownloadPath() {
512

513
    return this.downloadPath;
3✔
514
  }
515

516
  @Override
517
  public Path getUrlsPath() {
518

519
    return this.urlsPath;
3✔
520
  }
521

522
  @Override
523
  public Path getToolRepositoryPath() {
524

525
    return this.toolRepositoryPath;
3✔
526
  }
527

528
  @Override
529
  public SystemPath getPath() {
530

531
    return this.path;
3✔
532
  }
533

534
  @Override
535
  public EnvironmentVariables getVariables() {
536

537
    if (this.variables == null) {
3✔
538
      this.variables = createVariables();
4✔
539
    }
540
    return this.variables;
3✔
541
  }
542

543
  @Override
544
  public UrlMetadata getUrls() {
545

546
    if (this.urlMetadata == null) {
3✔
547
      if (!isTest()) {
3!
548
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, this.urlsPath, null);
×
549
      }
550
      this.urlMetadata = new UrlMetadata(this);
6✔
551
    }
552
    return this.urlMetadata;
3✔
553
  }
554

555
  @Override
556
  public boolean isQuietMode() {
557

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

561
  @Override
562
  public boolean isBatchMode() {
563

564
    return this.startContext.isBatchMode();
×
565
  }
566

567
  @Override
568
  public boolean isForceMode() {
569

570
    return this.startContext.isForceMode();
4✔
571
  }
572

573
  @Override
574
  public boolean isOfflineMode() {
575

576
    return this.startContext.isOfflineMode();
4✔
577
  }
578

579
  @Override
580
  public boolean isSkipUpdatesMode() {
581

582
    return this.startContext.isSkipUpdatesMode();
×
583
  }
584

585
  @Override
586
  public boolean isOnline() {
587

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

607
  private void configureNetworkProxy() {
608

609
    if (this.networkProxy == null) {
3✔
610
      this.networkProxy = new NetworkProxy(this);
6✔
611
      this.networkProxy.configure();
3✔
612
    }
613
  }
1✔
614

615
  @Override
616
  public Locale getLocale() {
617

618
    Locale locale = this.startContext.getLocale();
4✔
619
    if (locale == null) {
2!
620
      locale = Locale.getDefault();
×
621
    }
622
    return locale;
2✔
623
  }
624

625
  @Override
626
  public DirectoryMerger getWorkspaceMerger() {
627

628
    if (this.workspaceMerger == null) {
3✔
629
      this.workspaceMerger = new DirectoryMerger(this);
6✔
630
    }
631
    return this.workspaceMerger;
3✔
632
  }
633

634
  /**
635
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
636
   */
637
  @Override
638
  public Path getDefaultExecutionDirectory() {
639

640
    return this.defaultExecutionDirectory;
×
641
  }
642

643
  /**
644
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
645
   */
646
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
647

648
    if (defaultExecutionDirectory != null) {
×
649
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
650
    }
651
  }
×
652

653
  @Override
654
  public GitContext getGitContext() {
655

656
    return new GitContextImpl(this);
×
657
  }
658

659
  @Override
660
  public ProcessContext newProcess() {
661

662
    ProcessContext processContext = createProcessContext();
3✔
663
    if (this.defaultExecutionDirectory != null) {
3!
664
      processContext.directory(this.defaultExecutionDirectory);
×
665
    }
666
    return processContext;
2✔
667
  }
668

669
  @Override
670
  public IdeSystem getSystem() {
671

672
    if (this.system == null) {
×
673
      this.system = new IdeSystemImpl(this);
×
674
    }
675
    return this.system;
×
676
  }
677

678
  /**
679
   * @return a new instance of {@link ProcessContext}.
680
   * @see #newProcess()
681
   */
682
  protected ProcessContext createProcessContext() {
683

684
    return new ProcessContextImpl(this);
×
685
  }
686

687
  @Override
688
  public IdeSubLogger level(IdeLogLevel level) {
689

690
    return this.startContext.level(level);
5✔
691
  }
692

693
  @Override
694
  public void logIdeHomeAndRootStatus() {
695

696
    if (this.ideRoot != null) {
3!
697
      success("IDE_ROOT is set to {}", this.ideRoot);
×
698
    }
699
    if (this.ideHome == null) {
3!
700
      warning(getMessageIdeHomeNotFound());
5✔
701
    } else {
702
      success("IDE_HOME is set to {}", this.ideHome);
×
703
    }
704
  }
1✔
705

706
  @Override
707
  public String askForInput(String message, String defaultValue) {
708

709
    if (!message.isBlank()) {
×
710
      info(message);
×
711
    }
712
    if (isBatchMode()) {
×
713
      if (isForceMode()) {
×
714
        return defaultValue;
×
715
      } else {
716
        throw new CliAbortException();
×
717
      }
718
    }
719
    String input = readLine().trim();
×
720
    return input.isEmpty() ? defaultValue : input;
×
721
  }
722

723
  @Override
724
  public String askForInput(String message) {
725

726
    String input;
727
    do {
728
      info(message);
3✔
729
      input = readLine().trim();
4✔
730
    } while (input.isEmpty());
3!
731

732
    return input;
2✔
733
  }
734

735
  @SuppressWarnings("unchecked")
736
  @Override
737
  public <O> O question(String question, O... options) {
738

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

773
  /**
774
   * @return the input from the end-user (e.g. read from the console).
775
   */
776
  protected abstract String readLine();
777

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

780
    O duplicate = mapping.put(key, option);
×
781
    if (duplicate != null) {
×
782
      throw new IllegalArgumentException("Duplicated option " + key);
×
783
    }
784
  }
×
785

786
  @Override
787
  public Step getCurrentStep() {
788

789
    return this.currentStep;
×
790
  }
791

792
  @Override
793
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
794

795
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
796
    return this.currentStep;
3✔
797
  }
798

799
  /**
800
   * Internal method to end the running {@link Step}.
801
   *
802
   * @param step the current {@link Step} to end.
803
   */
804
  public void endStep(StepImpl step) {
805

806
    if (step == this.currentStep) {
4!
807
      this.currentStep = this.currentStep.getParent();
6✔
808
    } else {
809
      String currentStepName = "null";
×
810
      if (this.currentStep != null) {
×
811
        currentStepName = this.currentStep.getName();
×
812
      }
813
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
814
    }
815
  }
1✔
816

817
  /**
818
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
819
   *
820
   * @param arguments the {@link CliArgument}.
821
   * @return the return code of the execution.
822
   */
823
  public int run(CliArguments arguments) {
824

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

864
  /**
865
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
866
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
867
   *     {@link Commandlet} did not match and we have to try a different candidate).
868
   */
869
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
870

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

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

929
  private boolean ensureLicenseAgreement(Commandlet cmd) {
930

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

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

990
  private void verifyIdeRoot() {
991

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

1005
  /**
1006
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1007
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1008
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1009
   */
1010
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1011

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

1038
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1039

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

1096
  /**
1097
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1098
   *     {@link CliArguments#copy() copy} as needed.
1099
   * @param cmd the potential {@link Commandlet} to match.
1100
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1101
   */
1102
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1103

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

1150
  @Override
1151
  public String findBash() {
1152

1153
    String bash = "bash";
2✔
1154
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1155
      bash = findBashOnWindows();
×
1156
    }
1157

1158
    return bash;
2✔
1159
  }
1160

1161
  private String findBashOnWindows() {
1162

1163
    // Check if Git Bash exists in the default location
1164
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1165
    if (Files.exists(defaultPath)) {
×
1166
      return defaultPath.toString();
×
1167
    }
1168

1169
    // If not found in the default location, try the registry query
1170
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1171
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1172
    String regQueryResult;
1173
    for (String bashVariant : bashVariants) {
×
1174
      for (String registryKey : registryKeys) {
×
1175
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1176
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1177

1178
        try {
1179
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1180
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1181
            StringBuilder output = new StringBuilder();
×
1182
            String line;
1183

1184
            while ((line = reader.readLine()) != null) {
×
1185
              output.append(line);
×
1186
            }
1187

1188
            int exitCode = process.waitFor();
×
1189
            if (exitCode != 0) {
×
1190
              return null;
×
1191
            }
1192

1193
            regQueryResult = output.toString();
×
1194
            if (regQueryResult != null) {
×
1195
              int index = regQueryResult.indexOf("REG_SZ");
×
1196
              if (index != -1) {
×
1197
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1198
                return path + "\\bin\\bash.exe";
×
1199
              }
1200
            }
1201

1202
          }
×
1203
        } catch (Exception e) {
×
1204
          return null;
×
1205
        }
×
1206
      }
1207
    }
1208
    // no bash found
1209
    return null;
×
1210
  }
1211

1212
  @Override
1213
  public WindowsPathSyntax getPathSyntax() {
1214

1215
    return this.pathSyntax;
3✔
1216
  }
1217

1218
  /**
1219
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1220
   */
1221
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1222

1223
    this.pathSyntax = pathSyntax;
3✔
1224
  }
1✔
1225

1226
  /**
1227
   * @return the {@link IdeStartContextImpl}.
1228
   */
1229
  public IdeStartContextImpl getStartContext() {
1230

1231
    return startContext;
3✔
1232
  }
1233

1234
  /**
1235
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1236
   */
1237
  public void reload() {
1238

1239
    this.variables = null;
3✔
1240
    this.customToolRepository = null;
3✔
1241
  }
1✔
1242
}
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