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

devonfw / IDEasy / 12915070560

22 Jan 2025 06:52PM UTC coverage: 67.819% (-0.6%) from 68.444%
12915070560

Pull #957

github

web-flow
Merge bc519fb27 into d97386f6b
Pull Request #957: #786: Upgrade commandlet

2795 of 4511 branches covered (61.96%)

Branch coverage included in aggregate %.

7234 of 10277 relevant lines covered (70.39%)

3.06 hits per line

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

59.22
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
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
231
    if (root != null) {
×
232
      Path rootPath = Path.of(root);
×
233
      if (Files.isDirectory(rootPath)) {
×
234
        return rootPath;
×
235
      }
236
    }
237
    return null;
×
238
  }
239

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

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

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

278
  private String getMessageIdeHomeFound() {
279

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

283
  private String getMessageIdeHomeNotFound() {
284

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

288
  private String getMessageIdeRootNotFound() {
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

311
  private boolean isIdeHome(Path dir) {
312

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

321
  private EnvironmentVariables createVariables() {
322

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

331
  protected AbstractEnvironmentVariables createSystemVariables() {
332

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

336
  @Override
337
  public SystemInfo getSystemInfo() {
338

339
    return this.systemInfo;
3✔
340
  }
341

342
  @Override
343
  public FileAccess getFileAccess() {
344

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

350
  @Override
351
  public CommandletManager getCommandletManager() {
352

353
    return this.commandletManager;
3✔
354
  }
355

356
  @Override
357
  public ToolRepository getDefaultToolRepository() {
358

359
    return this.defaultToolRepository;
3✔
360
  }
361

362
  @Override
363
  public ToolRepository getMavenSoftwareRepository() {
364

365
    return this.mavenRepository;
×
366
  }
367

368
  @Override
369
  public CustomToolRepository getCustomToolRepository() {
370

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

377
  @Override
378
  public Path getIdeHome() {
379

380
    return this.ideHome;
3✔
381
  }
382

383
  @Override
384
  public String getProjectName() {
385

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

392
  @Override
393
  public Path getIdeRoot() {
394

395
    return this.ideRoot;
3✔
396
  }
397

398
  @Override
399
  public Path getCwd() {
400

401
    return this.cwd;
3✔
402
  }
403

404
  @Override
405
  public Path getTempPath() {
406

407
    return this.tempPath;
3✔
408
  }
409

410
  @Override
411
  public Path getTempDownloadPath() {
412

413
    return this.tempDownloadPath;
3✔
414
  }
415

416
  @Override
417
  public Path getUserHome() {
418

419
    return this.userHome;
3✔
420
  }
421

422
  @Override
423
  public Path getUserHomeIde() {
424

425
    return this.userHomeIde;
3✔
426
  }
427

428
  @Override
429
  public Path getSettingsPath() {
430

431
    return this.settingsPath;
3✔
432
  }
433

434
  @Override
435
  public Path getSettingsGitRepository() {
436

437
    Path settingsPath = getSettingsPath();
3✔
438

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

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

450
    return settingsPath;
×
451
  }
452

453
  @Override
454
  public Path getSettingsCommitIdPath() {
455

456
    return this.settingsCommitIdPath;
3✔
457
  }
458

459
  @Override
460
  public Path getConfPath() {
461

462
    return this.confPath;
3✔
463
  }
464

465
  @Override
466
  public Path getSoftwarePath() {
467

468
    return this.softwarePath;
3✔
469
  }
470

471
  @Override
472
  public Path getSoftwareExtraPath() {
473

474
    return this.softwareExtraPath;
3✔
475
  }
476

477
  @Override
478
  public Path getSoftwareRepositoryPath() {
479

480
    return this.softwareRepositoryPath;
3✔
481
  }
482

483
  @Override
484
  public Path getPluginsPath() {
485

486
    return this.pluginsPath;
3✔
487
  }
488

489
  @Override
490
  public String getWorkspaceName() {
491

492
    return this.workspaceName;
3✔
493
  }
494

495
  @Override
496
  public Path getWorkspacePath() {
497

498
    return this.workspacePath;
3✔
499
  }
500

501
  @Override
502
  public Path getDownloadPath() {
503

504
    return this.downloadPath;
3✔
505
  }
506

507
  @Override
508
  public Path getUrlsPath() {
509

510
    return this.urlsPath;
3✔
511
  }
512

513
  @Override
514
  public Path getToolRepositoryPath() {
515

516
    return this.toolRepositoryPath;
3✔
517
  }
518

519
  @Override
520
  public SystemPath getPath() {
521

522
    return this.path;
3✔
523
  }
524

525
  @Override
526
  public EnvironmentVariables getVariables() {
527

528
    if (this.variables == null) {
3✔
529
      this.variables = createVariables();
4✔
530
    }
531
    return this.variables;
3✔
532
  }
533

534
  @Override
535
  public UrlMetadata getUrls() {
536

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

546
  @Override
547
  public boolean isQuietMode() {
548

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

552
  @Override
553
  public boolean isBatchMode() {
554

555
    return this.startContext.isBatchMode();
×
556
  }
557

558
  @Override
559
  public boolean isForceMode() {
560

561
    return this.startContext.isForceMode();
4✔
562
  }
563

564
  @Override
565
  public boolean isOfflineMode() {
566

567
    return this.startContext.isOfflineMode();
4✔
568
  }
569

570
  @Override
571
  public boolean isSkipUpdatesMode() {
572
    return this.startContext.isSkipUpdatesMode();
×
573
  }
574

575
  @Override
576
  public boolean isOnline() {
577

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

597
  private void configureNetworkProxy() {
598
    if (this.networkProxy == null) {
3✔
599
      this.networkProxy = new NetworkProxy(this);
6✔
600
      this.networkProxy.configure();
3✔
601
    }
602
  }
1✔
603

604
  @Override
605
  public Locale getLocale() {
606

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

614
  @Override
615
  public DirectoryMerger getWorkspaceMerger() {
616

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

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

629
    return this.defaultExecutionDirectory;
×
630
  }
631

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

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

642
  @Override
643
  public GitContext getGitContext() {
644

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

648
  @Override
649
  public ProcessContext newProcess() {
650

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

658
  @Override
659
  public IdeSystem getSystem() {
660

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

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

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

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

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

682
  @Override
683
  public void logIdeHomeAndRootStatus() {
684

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

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

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

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

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

721
    return input;
2✔
722
  }
723

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

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

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

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

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

775
  @Override
776
  public Step getCurrentStep() {
777

778
    return this.currentStep;
×
779
  }
780

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

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

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

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

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

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

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

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

911
  private boolean ensureLicenseAgreement(Commandlet cmd) {
912

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

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

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

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

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

1075

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

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

1130
  @Override
1131
  public String findBash() {
1132

1133
    String bash = "bash";
2✔
1134
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1135
      bash = findBashOnWindows();
×
1136
    }
1137

1138
    return bash;
2✔
1139
  }
1140

1141
  private String findBashOnWindows() {
1142

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

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

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

1164
            while ((line = reader.readLine()) != null) {
×
1165
              output.append(line);
×
1166
            }
1167

1168
            int exitCode = process.waitFor();
×
1169
            if (exitCode != 0) {
×
1170
              return null;
×
1171
            }
1172

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

1182
          }
×
1183
        } catch (Exception e) {
×
1184
          return null;
×
1185
        }
×
1186
      }
1187
    }
1188
    // no bash found
1189
    return null;
×
1190
  }
1191

1192
  @Override
1193
  public WindowsPathSyntax getPathSyntax() {
1194
    return this.pathSyntax;
3✔
1195
  }
1196

1197
  /**
1198
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1199
   */
1200
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1201

1202
    this.pathSyntax = pathSyntax;
3✔
1203
  }
1✔
1204

1205
  /**
1206
   * @return the {@link IdeStartContextImpl}.
1207
   */
1208
  public IdeStartContextImpl getStartContext() {
1209

1210
    return startContext;
3✔
1211
  }
1212

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