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

devonfw / IDEasy / 13160073324

05 Feb 2025 02:52PM UTC coverage: 68.252% (-0.1%) from 68.379%
13160073324

Pull #1002

github

web-flow
Merge 61eb7829f into 62fa12bac
Pull Request #1002: #786: Upgrade commandlet

2901 of 4667 branches covered (62.16%)

Branch coverage included in aggregate %.

7504 of 10578 relevant lines covered (70.94%)

3.09 hits per line

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

59.42
cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
1
package com.devonfw.tools.ide.context;
2

3
import java.io.BufferedReader;
4
import java.io.InputStreamReader;
5
import java.net.URL;
6
import java.net.URLConnection;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.time.LocalDateTime;
10
import java.util.ArrayList;
11
import java.util.HashMap;
12
import java.util.Iterator;
13
import java.util.List;
14
import java.util.Locale;
15
import java.util.Map;
16

17
import com.devonfw.tools.ide.cli.CliAbortException;
18
import com.devonfw.tools.ide.cli.CliArgument;
19
import com.devonfw.tools.ide.cli.CliArguments;
20
import com.devonfw.tools.ide.cli.CliException;
21
import com.devonfw.tools.ide.commandlet.Commandlet;
22
import com.devonfw.tools.ide.commandlet.CommandletManager;
23
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
24
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
25
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
26
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
27
import com.devonfw.tools.ide.common.SystemPath;
28
import com.devonfw.tools.ide.completion.CompletionCandidate;
29
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
30
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
31
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
32
import com.devonfw.tools.ide.environment.EnvironmentVariables;
33
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
34
import com.devonfw.tools.ide.environment.IdeSystem;
35
import com.devonfw.tools.ide.environment.IdeSystemImpl;
36
import com.devonfw.tools.ide.git.GitContext;
37
import com.devonfw.tools.ide.git.GitContextImpl;
38
import com.devonfw.tools.ide.git.GitUrl;
39
import com.devonfw.tools.ide.io.FileAccess;
40
import com.devonfw.tools.ide.io.FileAccessImpl;
41
import com.devonfw.tools.ide.log.IdeLogLevel;
42
import com.devonfw.tools.ide.log.IdeLogger;
43
import com.devonfw.tools.ide.log.IdeSubLogger;
44
import com.devonfw.tools.ide.merge.DirectoryMerger;
45
import com.devonfw.tools.ide.network.NetworkProxy;
46
import com.devonfw.tools.ide.os.SystemInfo;
47
import com.devonfw.tools.ide.os.SystemInfoImpl;
48
import com.devonfw.tools.ide.os.WindowsPathSyntax;
49
import com.devonfw.tools.ide.process.ProcessContext;
50
import com.devonfw.tools.ide.process.ProcessContextImpl;
51
import com.devonfw.tools.ide.process.ProcessResult;
52
import com.devonfw.tools.ide.property.Property;
53
import com.devonfw.tools.ide.step.Step;
54
import com.devonfw.tools.ide.step.StepImpl;
55
import com.devonfw.tools.ide.tool.repository.CustomToolRepository;
56
import com.devonfw.tools.ide.tool.repository.CustomToolRepositoryImpl;
57
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
58
import com.devonfw.tools.ide.tool.repository.MavenRepository;
59
import com.devonfw.tools.ide.tool.repository.ToolRepository;
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 final Path ideInstallationPath;
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.ideInstallationPath = null;
3✔
196
      this.toolRepositoryPath = null;
3✔
197
      this.urlsPath = null;
3✔
198
      this.tempPath = null;
3✔
199
      this.tempDownloadPath = null;
3✔
200
      this.softwareRepositoryPath = null;
4✔
201
    } else {
202
      this.ideInstallationPath = this.ideRoot.resolve(FOLDER_IDE_INSTALLATION);
6✔
203
      this.toolRepositoryPath = this.ideInstallationPath.resolve("software");
6✔
204
      this.urlsPath = this.ideInstallationPath.resolve("urls");
6✔
205
      this.tempPath = this.ideInstallationPath.resolve("tmp");
6✔
206
      this.tempDownloadPath = this.tempPath.resolve(FOLDER_DOWNLOADS);
6✔
207
      this.softwareRepositoryPath = this.ideInstallationPath.resolve(FOLDER_SOFTWARE);
6✔
208
      if (Files.isDirectory(this.tempPath)) {
7✔
209
        // TODO delete all files older than 1 day here...
210
      } else {
211
        this.fileAccess.mkdirs(this.tempDownloadPath);
5✔
212
      }
213
    }
214

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

219
  private Path findIdeRoot(Path ideHomePath) {
220

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

230
  private Path getIdeRootPathFromEnv() {
231

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

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

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

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

280
  private String getMessageIdeHomeFound() {
281

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

285
  private String getMessageIdeHomeNotFound() {
286

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

290
  private String getMessageIdeRootNotFound() {
291

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

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

305
    return false;
×
306
  }
307

308
  protected SystemPath computeSystemPath() {
309

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

313
  private boolean isIdeHome(Path dir) {
314

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

323
  private EnvironmentVariables createVariables() {
324

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

333
  protected AbstractEnvironmentVariables createSystemVariables() {
334

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

338
  @Override
339
  public SystemInfo getSystemInfo() {
340

341
    return this.systemInfo;
3✔
342
  }
343

344
  @Override
345
  public FileAccess getFileAccess() {
346

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

352
  @Override
353
  public CommandletManager getCommandletManager() {
354

355
    return this.commandletManager;
3✔
356
  }
357

358
  @Override
359
  public ToolRepository getDefaultToolRepository() {
360

361
    return this.defaultToolRepository;
3✔
362
  }
363

364
  @Override
365
  public MavenRepository getMavenToolRepository() {
366

367
    return this.mavenRepository;
3✔
368
  }
369

370
  @Override
371
  public CustomToolRepository getCustomToolRepository() {
372

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

379
  @Override
380
  public Path getIdeHome() {
381

382
    return this.ideHome;
3✔
383
  }
384

385
  @Override
386
  public String getProjectName() {
387

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

394
  @Override
395
  public Path getIdeRoot() {
396

397
    return this.ideRoot;
3✔
398
  }
399

400
  @Override
401
  public Path getIdeInstallationPath() {
402

403
    return this.ideInstallationPath;
×
404
  }
405

406
  @Override
407
  public Path getCwd() {
408

409
    return this.cwd;
3✔
410
  }
411

412
  @Override
413
  public Path getTempPath() {
414

415
    return this.tempPath;
3✔
416
  }
417

418
  @Override
419
  public Path getTempDownloadPath() {
420

421
    return this.tempDownloadPath;
3✔
422
  }
423

424
  @Override
425
  public Path getUserHome() {
426

427
    return this.userHome;
3✔
428
  }
429

430
  @Override
431
  public Path getUserHomeIde() {
432

433
    return this.userHomeIde;
3✔
434
  }
435

436
  @Override
437
  public Path getSettingsPath() {
438

439
    return this.settingsPath;
3✔
440
  }
441

442
  @Override
443
  public Path getSettingsGitRepository() {
444

445
    Path settingsPath = getSettingsPath();
3✔
446

447
    if (settingsPath == null) {
2✔
448
      error("No settings repository was found.");
3✔
449
      return null;
2✔
450
    }
451

452
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
453
    if (!Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
10!
454
      error("Settings repository exists but is not a git repository.");
3✔
455
      return null;
2✔
456
    }
457

458
    return settingsPath;
×
459
  }
460

461
  @Override
462
  public boolean isSettingsRepositorySymlinkOrJunction() {
463

464
    Path settingsPath = getSettingsPath();
3✔
465
    if (settingsPath == null) {
2!
466
      return false;
×
467
    }
468
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
469
  }
470

471
  @Override
472
  public Path getSettingsCommitIdPath() {
473

474
    return this.settingsCommitIdPath;
3✔
475
  }
476

477
  @Override
478
  public Path getConfPath() {
479

480
    return this.confPath;
3✔
481
  }
482

483
  @Override
484
  public Path getSoftwarePath() {
485

486
    return this.softwarePath;
3✔
487
  }
488

489
  @Override
490
  public Path getSoftwareExtraPath() {
491

492
    return this.softwareExtraPath;
3✔
493
  }
494

495
  @Override
496
  public Path getSoftwareRepositoryPath() {
497

498
    return this.softwareRepositoryPath;
3✔
499
  }
500

501
  @Override
502
  public Path getPluginsPath() {
503

504
    return this.pluginsPath;
3✔
505
  }
506

507
  @Override
508
  public String getWorkspaceName() {
509

510
    return this.workspaceName;
3✔
511
  }
512

513
  @Override
514
  public Path getWorkspacePath() {
515

516
    return this.workspacePath;
3✔
517
  }
518

519
  @Override
520
  public Path getDownloadPath() {
521

522
    return this.downloadPath;
3✔
523
  }
524

525
  @Override
526
  public Path getUrlsPath() {
527

528
    return this.urlsPath;
3✔
529
  }
530

531
  @Override
532
  public Path getToolRepositoryPath() {
533

534
    return this.toolRepositoryPath;
3✔
535
  }
536

537
  @Override
538
  public SystemPath getPath() {
539

540
    return this.path;
3✔
541
  }
542

543
  @Override
544
  public EnvironmentVariables getVariables() {
545

546
    if (this.variables == null) {
3✔
547
      this.variables = createVariables();
4✔
548
    }
549
    return this.variables;
3✔
550
  }
551

552
  @Override
553
  public UrlMetadata getUrls() {
554

555
    if (this.urlMetadata == null) {
3✔
556
      if (!isTest()) {
3!
557
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, this.urlsPath, null);
×
558
      }
559
      this.urlMetadata = new UrlMetadata(this);
6✔
560
    }
561
    return this.urlMetadata;
3✔
562
  }
563

564
  @Override
565
  public boolean isQuietMode() {
566

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

570
  @Override
571
  public boolean isBatchMode() {
572

573
    return this.startContext.isBatchMode();
×
574
  }
575

576
  @Override
577
  public boolean isForceMode() {
578

579
    return this.startContext.isForceMode();
4✔
580
  }
581

582
  @Override
583
  public boolean isOfflineMode() {
584

585
    return this.startContext.isOfflineMode();
4✔
586
  }
587

588
  @Override
589
  public boolean isSkipUpdatesMode() {
590

591
    return this.startContext.isSkipUpdatesMode();
×
592
  }
593

594
  @Override
595
  public boolean isOnline() {
596

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

616
  private void configureNetworkProxy() {
617

618
    if (this.networkProxy == null) {
3✔
619
      this.networkProxy = new NetworkProxy(this);
6✔
620
      this.networkProxy.configure();
3✔
621
    }
622
  }
1✔
623

624
  @Override
625
  public Locale getLocale() {
626

627
    Locale locale = this.startContext.getLocale();
4✔
628
    if (locale == null) {
2!
629
      locale = Locale.getDefault();
×
630
    }
631
    return locale;
2✔
632
  }
633

634
  @Override
635
  public DirectoryMerger getWorkspaceMerger() {
636

637
    if (this.workspaceMerger == null) {
3✔
638
      this.workspaceMerger = new DirectoryMerger(this);
6✔
639
    }
640
    return this.workspaceMerger;
3✔
641
  }
642

643
  /**
644
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
645
   */
646
  @Override
647
  public Path getDefaultExecutionDirectory() {
648

649
    return this.defaultExecutionDirectory;
×
650
  }
651

652
  /**
653
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
654
   */
655
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
656

657
    if (defaultExecutionDirectory != null) {
×
658
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
659
    }
660
  }
×
661

662
  @Override
663
  public GitContext getGitContext() {
664

665
    return new GitContextImpl(this);
×
666
  }
667

668
  @Override
669
  public ProcessContext newProcess() {
670

671
    ProcessContext processContext = createProcessContext();
3✔
672
    if (this.defaultExecutionDirectory != null) {
3!
673
      processContext.directory(this.defaultExecutionDirectory);
×
674
    }
675
    return processContext;
2✔
676
  }
677

678
  @Override
679
  public IdeSystem getSystem() {
680

681
    if (this.system == null) {
×
682
      this.system = new IdeSystemImpl(this);
×
683
    }
684
    return this.system;
×
685
  }
686

687
  /**
688
   * @return a new instance of {@link ProcessContext}.
689
   * @see #newProcess()
690
   */
691
  protected ProcessContext createProcessContext() {
692

693
    return new ProcessContextImpl(this);
×
694
  }
695

696
  @Override
697
  public IdeSubLogger level(IdeLogLevel level) {
698

699
    return this.startContext.level(level);
5✔
700
  }
701

702
  @Override
703
  public void logIdeHomeAndRootStatus() {
704

705
    if (this.ideRoot != null) {
3!
706
      success("IDE_ROOT is set to {}", this.ideRoot);
×
707
    }
708
    if (this.ideHome == null) {
3!
709
      warning(getMessageIdeHomeNotFound());
5✔
710
    } else {
711
      success("IDE_HOME is set to {}", this.ideHome);
×
712
    }
713
  }
1✔
714

715
  @Override
716
  public String askForInput(String message, String defaultValue) {
717

718
    if (!message.isBlank()) {
×
719
      info(message);
×
720
    }
721
    if (isBatchMode()) {
×
722
      if (isForceMode()) {
×
723
        return defaultValue;
×
724
      } else {
725
        throw new CliAbortException();
×
726
      }
727
    }
728
    String input = readLine().trim();
×
729
    return input.isEmpty() ? defaultValue : input;
×
730
  }
731

732
  @Override
733
  public String askForInput(String message) {
734

735
    String input;
736
    do {
737
      info(message);
3✔
738
      input = readLine().trim();
4✔
739
    } while (input.isEmpty());
3!
740

741
    return input;
2✔
742
  }
743

744
  @SuppressWarnings("unchecked")
745
  @Override
746
  public <O> O question(String question, O... options) {
747

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

782
  /**
783
   * @return the input from the end-user (e.g. read from the console).
784
   */
785
  protected abstract String readLine();
786

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

789
    O duplicate = mapping.put(key, option);
×
790
    if (duplicate != null) {
×
791
      throw new IllegalArgumentException("Duplicated option " + key);
×
792
    }
793
  }
×
794

795
  @Override
796
  public Step getCurrentStep() {
797

798
    return this.currentStep;
×
799
  }
800

801
  @Override
802
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
803

804
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
805
    return this.currentStep;
3✔
806
  }
807

808
  /**
809
   * Internal method to end the running {@link Step}.
810
   *
811
   * @param step the current {@link Step} to end.
812
   */
813
  public void endStep(StepImpl step) {
814

815
    if (step == this.currentStep) {
4!
816
      this.currentStep = this.currentStep.getParent();
6✔
817
    } else {
818
      String currentStepName = "null";
×
819
      if (this.currentStep != null) {
×
820
        currentStepName = this.currentStep.getName();
×
821
      }
822
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
823
    }
824
  }
1✔
825

826
  /**
827
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
828
   *
829
   * @param arguments the {@link CliArgument}.
830
   * @return the return code of the execution.
831
   */
832
  public int run(CliArguments arguments) {
833

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

873
  /**
874
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
875
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
876
   *     {@link Commandlet} did not match and we have to try a different candidate).
877
   */
878
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
879

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

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

938
  private boolean ensureLicenseAgreement(Commandlet cmd) {
939

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

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

999
  private void verifyIdeRoot() {
1000

1001
    if (!isTest()) {
3!
1002
      if (this.ideRoot == null) {
×
1003
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
1004
      } else if (this.ideHome != null) {
×
1005
        Path ideRootPath = getIdeRootPathFromEnv();
×
1006
        if (!this.ideRoot.equals(ideRootPath)) {
×
1007
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath,
×
1008
              this.ideHome.getFileName(), this.ideRoot);
×
1009
        }
1010
      }
1011
    }
1012
  }
1✔
1013

1014
  /**
1015
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1016
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1017
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1018
   */
1019
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1020

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

1047
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1048

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

1105
  /**
1106
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1107
   *     {@link CliArguments#copy() copy} as needed.
1108
   * @param cmd the potential {@link Commandlet} to match.
1109
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1110
   */
1111
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1112

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

1159
  @Override
1160
  public String findBash() {
1161

1162
    String bash = "bash";
2✔
1163
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1164
      bash = findBashOnWindows();
×
1165
    }
1166

1167
    return bash;
2✔
1168
  }
1169

1170
  private String findBashOnWindows() {
1171

1172
    // Check if Git Bash exists in the default location
1173
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1174
    if (Files.exists(defaultPath)) {
×
1175
      return defaultPath.toString();
×
1176
    }
1177

1178
    // If not found in the default location, try the registry query
1179
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1180
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1181
    String regQueryResult;
1182
    for (String bashVariant : bashVariants) {
×
1183
      for (String registryKey : registryKeys) {
×
1184
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1185
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1186

1187
        try {
1188
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1189
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1190
            StringBuilder output = new StringBuilder();
×
1191
            String line;
1192

1193
            while ((line = reader.readLine()) != null) {
×
1194
              output.append(line);
×
1195
            }
1196

1197
            int exitCode = process.waitFor();
×
1198
            if (exitCode != 0) {
×
1199
              return null;
×
1200
            }
1201

1202
            regQueryResult = output.toString();
×
1203
            if (regQueryResult != null) {
×
1204
              int index = regQueryResult.indexOf("REG_SZ");
×
1205
              if (index != -1) {
×
1206
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1207
                return path + "\\bin\\bash.exe";
×
1208
              }
1209
            }
1210

1211
          }
×
1212
        } catch (Exception e) {
×
1213
          return null;
×
1214
        }
×
1215
      }
1216
    }
1217
    // no bash found
1218
    return null;
×
1219
  }
1220

1221
  @Override
1222
  public WindowsPathSyntax getPathSyntax() {
1223

1224
    return this.pathSyntax;
3✔
1225
  }
1226

1227
  /**
1228
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1229
   */
1230
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1231

1232
    this.pathSyntax = pathSyntax;
3✔
1233
  }
1✔
1234

1235
  /**
1236
   * @return the {@link IdeStartContextImpl}.
1237
   */
1238
  public IdeStartContextImpl getStartContext() {
1239

1240
    return startContext;
3✔
1241
  }
1242

1243
  /**
1244
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1245
   */
1246
  public void reload() {
1247

1248
    this.variables = null;
3✔
1249
    this.customToolRepository = null;
3✔
1250
  }
1✔
1251
}
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