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

devonfw / IDEasy / 13038836781

29 Jan 2025 07:22PM UTC coverage: 68.163% (-0.3%) from 68.45%
13038836781

Pull #957

github

web-flow
Merge cc3109b40 into 5300a96fe
Pull Request #957: #786: Upgrade commandlet

2833 of 4553 branches covered (62.22%)

Branch coverage included in aggregate %.

7324 of 10348 relevant lines covered (70.78%)

3.08 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
import javax.tools.Tool;
68

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

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

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

78
  private final IdeStartContextImpl startContext;
79

80
  private Path ideHome;
81

82
  private final Path ideRoot;
83

84
  private Path confPath;
85

86
  protected Path settingsPath;
87

88
  private Path settingsCommitIdPath;
89

90
  private Path softwarePath;
91

92
  private Path softwareExtraPath;
93

94
  private final Path softwareRepositoryPath;
95

96
  protected Path pluginsPath;
97

98
  private Path workspacePath;
99

100
  private String workspaceName;
101

102
  protected Path urlsPath;
103

104
  private final Path tempPath;
105

106
  private final Path tempDownloadPath;
107

108
  private Path cwd;
109

110
  private Path downloadPath;
111

112
  private final Path toolRepositoryPath;
113

114
  protected Path userHome;
115

116
  private Path userHomeIde;
117

118
  private SystemPath path;
119

120
  private WindowsPathSyntax pathSyntax;
121

122
  private final SystemInfo systemInfo;
123

124
  private EnvironmentVariables variables;
125

126
  private final FileAccess fileAccess;
127

128
  protected CommandletManager commandletManager;
129

130
  protected ToolRepository defaultToolRepository;
131

132
  private CustomToolRepository customToolRepository;
133

134
  private MavenRepository mavenRepository;
135

136
  private DirectoryMerger workspaceMerger;
137

138
  protected UrlMetadata urlMetadata;
139

140
  protected Path defaultExecutionDirectory;
141

142
  private StepImpl currentStep;
143

144
  protected Boolean online;
145

146
  protected IdeSystem system;
147

148
  private NetworkProxy networkProxy;
149

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

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

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

192
    setCwd(workingDirectory, workspace, currentDir);
5✔
193

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

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

218
  private Path findIdeRoot(Path ideHomePath) {
219

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

229
  private Path getIdeRootPathFromEnv() {
230

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

241
  @Override
242
  public void setCwd(Path userDir, String workspace, Path ideHome) {
243

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

276
    this.path = computeSystemPath();
4✔
277
  }
1✔
278

279
  private String getMessageIdeHomeFound() {
280

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

284
  private String getMessageIdeHomeNotFound() {
285

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

289
  private String getMessageIdeRootNotFound() {
290

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

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

304
    return false;
×
305
  }
306

307
  protected SystemPath computeSystemPath() {
308

309
    return new SystemPath(this);
×
310
  }
311

312
  private boolean isIdeHome(Path dir) {
313

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

322
  private EnvironmentVariables createVariables() {
323

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

332
  protected AbstractEnvironmentVariables createSystemVariables() {
333

334
    return EnvironmentVariables.ofSystem(this);
3✔
335
  }
336

337
  @Override
338
  public SystemInfo getSystemInfo() {
339

340
    return this.systemInfo;
3✔
341
  }
342

343
  @Override
344
  public FileAccess getFileAccess() {
345

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

351
  @Override
352
  public CommandletManager getCommandletManager() {
353

354
    return this.commandletManager;
3✔
355
  }
356

357
  @Override
358
  public ToolRepository getDefaultToolRepository() {
359

360
    return this.defaultToolRepository;
3✔
361
  }
362

363
  @Override
364
  public ToolRepository getMavenSoftwareRepository() {
365

366
    return this.mavenRepository;
×
367
  }
368

369
  @Override
370
  public CustomToolRepository getCustomToolRepository() {
371

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

378
  @Override
379
  public Path getIdeHome() {
380

381
    return this.ideHome;
3✔
382
  }
383

384
  @Override
385
  public String getProjectName() {
386

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

393
  @Override
394
  public Path getIdeRoot() {
395

396
    return this.ideRoot;
3✔
397
  }
398

399
  @Override
400
  public Path getCwd() {
401

402
    return this.cwd;
3✔
403
  }
404

405
  @Override
406
  public Path getTempPath() {
407

408
    return this.tempPath;
3✔
409
  }
410

411
  @Override
412
  public Path getTempDownloadPath() {
413

414
    return this.tempDownloadPath;
3✔
415
  }
416

417
  @Override
418
  public Path getUserHome() {
419

420
    return this.userHome;
3✔
421
  }
422

423
  @Override
424
  public Path getUserHomeIde() {
425

426
    return this.userHomeIde;
3✔
427
  }
428

429
  @Override
430
  public Path getSettingsPath() {
431

432
    return this.settingsPath;
3✔
433
  }
434

435
  @Override
436
  public Path getSettingsGitRepository() {
437

438
    Path settingsPath = getSettingsPath();
3✔
439

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

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

451
    return settingsPath;
×
452
  }
453

454
  public boolean isSettingsRepositorySymlinkOrJunction() {
455

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

463
  @Override
464
  public Path getSettingsCommitIdPath() {
465

466
    return this.settingsCommitIdPath;
3✔
467
  }
468

469
  @Override
470
  public Path getConfPath() {
471

472
    return this.confPath;
3✔
473
  }
474

475
  @Override
476
  public Path getSoftwarePath() {
477

478
    return this.softwarePath;
3✔
479
  }
480

481
  @Override
482
  public Path getSoftwareExtraPath() {
483

484
    return this.softwareExtraPath;
3✔
485
  }
486

487
  @Override
488
  public Path getSoftwareRepositoryPath() {
489

490
    return this.softwareRepositoryPath;
3✔
491
  }
492

493
  @Override
494
  public Path getPluginsPath() {
495

496
    return this.pluginsPath;
3✔
497
  }
498

499
  @Override
500
  public String getWorkspaceName() {
501

502
    return this.workspaceName;
3✔
503
  }
504

505
  @Override
506
  public Path getWorkspacePath() {
507

508
    return this.workspacePath;
3✔
509
  }
510

511
  @Override
512
  public Path getDownloadPath() {
513

514
    return this.downloadPath;
3✔
515
  }
516

517
  @Override
518
  public Path getUrlsPath() {
519

520
    return this.urlsPath;
3✔
521
  }
522

523
  @Override
524
  public Path getToolRepositoryPath() {
525

526
    return this.toolRepositoryPath;
3✔
527
  }
528

529
  @Override
530
  public SystemPath getPath() {
531

532
    return this.path;
3✔
533
  }
534

535
  @Override
536
  public EnvironmentVariables getVariables() {
537

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

544
  @Override
545
  public UrlMetadata getUrls() {
546

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

556
  @Override
557
  public boolean isQuietMode() {
558

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

562
  @Override
563
  public boolean isBatchMode() {
564

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

568
  @Override
569
  public boolean isForceMode() {
570

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

574
  @Override
575
  public boolean isOfflineMode() {
576

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

580
  @Override
581
  public boolean isSkipUpdatesMode() {
582

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

586
  @Override
587
  public boolean isOnline() {
588

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

608
  private void configureNetworkProxy() {
609

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

616
  @Override
617
  public Locale getLocale() {
618

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

626
  @Override
627
  public DirectoryMerger getWorkspaceMerger() {
628

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

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

641
    return this.defaultExecutionDirectory;
×
642
  }
643

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

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

654
  @Override
655
  public GitContext getGitContext() {
656

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

660
  @Override
661
  public ProcessContext newProcess() {
662

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

670
  @Override
671
  public IdeSystem getSystem() {
672

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

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

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

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

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

694
  @Override
695
  public void logIdeHomeAndRootStatus() {
696

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

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

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

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

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

733
    return input;
2✔
734
  }
735

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

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

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

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

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

787
  @Override
788
  public Step getCurrentStep() {
789

790
    return this.currentStep;
×
791
  }
792

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

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

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

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

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

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

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

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

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

930
  private boolean ensureLicenseAgreement(Commandlet cmd) {
931

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

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

991
  private void verifyIdeRoot() {
992

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

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

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

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

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

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

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

1151
  @Override
1152
  public String findBash() {
1153

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

1159
    return bash;
2✔
1160
  }
1161

1162
  private String findBashOnWindows() {
1163

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

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

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

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

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

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

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

1213
  @Override
1214
  public WindowsPathSyntax getPathSyntax() {
1215

1216
    return this.pathSyntax;
3✔
1217
  }
1218

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

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

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

1232
    return startContext;
3✔
1233
  }
1234

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

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

© 2025 Coveralls, Inc