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

devonfw / IDEasy / 12995336297

27 Jan 2025 06:04PM UTC coverage: 68.45% (-0.05%) from 68.499%
12995336297

push

github

web-flow
#931: enhance settings in code repository (#983)

Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>
Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

2793 of 4475 branches covered (62.41%)

Branch coverage included in aggregate %.

7211 of 10140 relevant lines covered (71.11%)

3.09 hits per line

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

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

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

17
import com.devonfw.tools.ide.cli.CliAbortException;
18
import com.devonfw.tools.ide.cli.CliArgument;
19
import com.devonfw.tools.ide.cli.CliArguments;
20
import com.devonfw.tools.ide.cli.CliException;
21
import com.devonfw.tools.ide.commandlet.Commandlet;
22
import com.devonfw.tools.ide.commandlet.CommandletManager;
23
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
24
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
25
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
26
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
27
import com.devonfw.tools.ide.common.SystemPath;
28
import com.devonfw.tools.ide.completion.CompletionCandidate;
29
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
30
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
31
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
32
import com.devonfw.tools.ide.environment.EnvironmentVariables;
33
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
34
import com.devonfw.tools.ide.environment.IdeSystem;
35
import com.devonfw.tools.ide.environment.IdeSystemImpl;
36
import com.devonfw.tools.ide.git.GitContext;
37
import com.devonfw.tools.ide.git.GitContextImpl;
38
import com.devonfw.tools.ide.git.GitUrl;
39
import com.devonfw.tools.ide.io.FileAccess;
40
import com.devonfw.tools.ide.io.FileAccessImpl;
41
import com.devonfw.tools.ide.log.IdeLogLevel;
42
import com.devonfw.tools.ide.log.IdeLogger;
43
import com.devonfw.tools.ide.log.IdeSubLogger;
44
import com.devonfw.tools.ide.merge.DirectoryMerger;
45
import com.devonfw.tools.ide.network.NetworkProxy;
46
import com.devonfw.tools.ide.os.SystemInfo;
47
import com.devonfw.tools.ide.os.SystemInfoImpl;
48
import com.devonfw.tools.ide.os.WindowsPathSyntax;
49
import com.devonfw.tools.ide.process.ProcessContext;
50
import com.devonfw.tools.ide.process.ProcessContextImpl;
51
import com.devonfw.tools.ide.process.ProcessResult;
52
import com.devonfw.tools.ide.property.Property;
53
import com.devonfw.tools.ide.repo.CustomToolRepository;
54
import com.devonfw.tools.ide.repo.CustomToolRepositoryImpl;
55
import com.devonfw.tools.ide.repo.DefaultToolRepository;
56
import com.devonfw.tools.ide.repo.ToolRepository;
57
import com.devonfw.tools.ide.step.Step;
58
import com.devonfw.tools.ide.step.StepImpl;
59
import com.devonfw.tools.ide.url.model.UrlMetadata;
60
import com.devonfw.tools.ide.util.DateTimeUtil;
61
import com.devonfw.tools.ide.validation.ValidationResult;
62
import com.devonfw.tools.ide.validation.ValidationResultValid;
63
import com.devonfw.tools.ide.validation.ValidationState;
64
import com.devonfw.tools.ide.variable.IdeVariables;
65

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

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

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

75
  private final IdeStartContextImpl startContext;
76

77
  private Path ideHome;
78

79
  private final Path ideRoot;
80

81
  private Path confPath;
82

83
  protected Path settingsPath;
84

85
  private Path settingsCommitIdPath;
86

87
  private Path softwarePath;
88

89
  private Path softwareExtraPath;
90

91
  private final Path softwareRepositoryPath;
92

93
  protected Path pluginsPath;
94

95
  private Path workspacePath;
96

97
  private String workspaceName;
98

99
  protected Path urlsPath;
100

101
  private final Path tempPath;
102

103
  private final Path tempDownloadPath;
104

105
  private Path cwd;
106

107
  private Path downloadPath;
108

109
  private final Path toolRepositoryPath;
110

111
  protected Path userHome;
112

113
  private Path userHomeIde;
114

115
  private SystemPath path;
116

117
  private WindowsPathSyntax pathSyntax;
118

119
  private final SystemInfo systemInfo;
120

121
  private EnvironmentVariables variables;
122

123
  private final FileAccess fileAccess;
124

125
  protected CommandletManager commandletManager;
126

127
  protected ToolRepository defaultToolRepository;
128

129
  private CustomToolRepository customToolRepository;
130

131
  private DirectoryMerger workspaceMerger;
132

133
  protected UrlMetadata urlMetadata;
134

135
  protected Path defaultExecutionDirectory;
136

137
  private StepImpl currentStep;
138

139
  protected Boolean online;
140

141
  protected IdeSystem system;
142

143
  private NetworkProxy networkProxy;
144

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

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

184
    // detection completed, initializing variables
185
    this.ideRoot = findIdeRoot(currentDir);
5✔
186

187
    setCwd(workingDirectory, workspace, currentDir);
5✔
188

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

209
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
210
  }
1✔
211

212
  private Path findIdeRoot(Path ideHomePath) {
213

214
    Path ideRootPath = null;
2✔
215
    if (ideHomePath != null) {
2✔
216
      ideRootPath = ideHomePath.getParent();
4✔
217
    } else if (!isTest()) {
3!
218
      ideRootPath = getIdeRootPathFromEnv();
×
219
    }
220
    return ideRootPath;
2✔
221
  }
222

223
  private Path getIdeRootPathFromEnv() {
224

225
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
226
    if (root != null) {
×
227
      Path rootPath = Path.of(root);
×
228
      if (Files.isDirectory(rootPath)) {
×
229
        return rootPath;
×
230
      }
231
    }
232
    return null;
×
233
  }
234

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

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

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

273
  private String getMessageIdeHomeFound() {
274

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

278
  private String getMessageIdeHomeNotFound() {
279

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

283
  private String getMessageIdeRootNotFound() {
284

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

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

298
    return false;
×
299
  }
300

301
  protected SystemPath computeSystemPath() {
302

303
    return new SystemPath(this);
×
304
  }
305

306
  private boolean isIdeHome(Path dir) {
307

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

316
  private EnvironmentVariables createVariables() {
317

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

326
  protected AbstractEnvironmentVariables createSystemVariables() {
327

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

331
  @Override
332
  public SystemInfo getSystemInfo() {
333

334
    return this.systemInfo;
3✔
335
  }
336

337
  @Override
338
  public FileAccess getFileAccess() {
339

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

345
  @Override
346
  public CommandletManager getCommandletManager() {
347

348
    return this.commandletManager;
3✔
349
  }
350

351
  @Override
352
  public ToolRepository getDefaultToolRepository() {
353

354
    return this.defaultToolRepository;
3✔
355
  }
356

357
  @Override
358
  public CustomToolRepository getCustomToolRepository() {
359

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

366
  @Override
367
  public Path getIdeHome() {
368

369
    return this.ideHome;
3✔
370
  }
371

372
  @Override
373
  public String getProjectName() {
374

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

381
  @Override
382
  public Path getIdeRoot() {
383

384
    return this.ideRoot;
3✔
385
  }
386

387
  @Override
388
  public Path getCwd() {
389

390
    return this.cwd;
3✔
391
  }
392

393
  @Override
394
  public Path getTempPath() {
395

396
    return this.tempPath;
3✔
397
  }
398

399
  @Override
400
  public Path getTempDownloadPath() {
401

402
    return this.tempDownloadPath;
3✔
403
  }
404

405
  @Override
406
  public Path getUserHome() {
407

408
    return this.userHome;
3✔
409
  }
410

411
  @Override
412
  public Path getUserHomeIde() {
413

414
    return this.userHomeIde;
3✔
415
  }
416

417
  @Override
418
  public Path getSettingsPath() {
419

420
    return this.settingsPath;
3✔
421
  }
422

423
  @Override
424
  public Path getSettingsGitRepository() {
425

426
    Path settingsPath = getSettingsPath();
3✔
427

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

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

439
    return settingsPath;
×
440
  }
441

442
  public boolean isSettingsRepositorySymlinkOrJunction() {
443

444
    Path settingsPath = getSettingsPath();
3✔
445
    if (settingsPath == null) {
2!
446
      return false;
×
447
    }
448
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
449
  }
450

451
  @Override
452
  public Path getSettingsCommitIdPath() {
453

454
    return this.settingsCommitIdPath;
3✔
455
  }
456

457
  @Override
458
  public Path getConfPath() {
459

460
    return this.confPath;
3✔
461
  }
462

463
  @Override
464
  public Path getSoftwarePath() {
465

466
    return this.softwarePath;
3✔
467
  }
468

469
  @Override
470
  public Path getSoftwareExtraPath() {
471

472
    return this.softwareExtraPath;
3✔
473
  }
474

475
  @Override
476
  public Path getSoftwareRepositoryPath() {
477

478
    return this.softwareRepositoryPath;
3✔
479
  }
480

481
  @Override
482
  public Path getPluginsPath() {
483

484
    return this.pluginsPath;
3✔
485
  }
486

487
  @Override
488
  public String getWorkspaceName() {
489

490
    return this.workspaceName;
3✔
491
  }
492

493
  @Override
494
  public Path getWorkspacePath() {
495

496
    return this.workspacePath;
3✔
497
  }
498

499
  @Override
500
  public Path getDownloadPath() {
501

502
    return this.downloadPath;
3✔
503
  }
504

505
  @Override
506
  public Path getUrlsPath() {
507

508
    return this.urlsPath;
3✔
509
  }
510

511
  @Override
512
  public Path getToolRepositoryPath() {
513

514
    return this.toolRepositoryPath;
3✔
515
  }
516

517
  @Override
518
  public SystemPath getPath() {
519

520
    return this.path;
3✔
521
  }
522

523
  @Override
524
  public EnvironmentVariables getVariables() {
525

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

532
  @Override
533
  public UrlMetadata getUrls() {
534

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

544
  @Override
545
  public boolean isQuietMode() {
546

547
    return this.startContext.isQuietMode();
4✔
548
  }
549

550
  @Override
551
  public boolean isBatchMode() {
552

553
    return this.startContext.isBatchMode();
×
554
  }
555

556
  @Override
557
  public boolean isForceMode() {
558

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

562
  @Override
563
  public boolean isOfflineMode() {
564

565
    return this.startContext.isOfflineMode();
4✔
566
  }
567

568
  @Override
569
  public boolean isSkipUpdatesMode() {
570

571
    return this.startContext.isSkipUpdatesMode();
×
572
  }
573

574
  @Override
575
  public boolean isOnline() {
576

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

596
  private void configureNetworkProxy() {
597

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(
×
890
                    settingsRepository, getSettingsCommitIdPath()))) {
×
891
              if (isSettingsRepositorySymlinkOrJunction()) {
×
892
                interaction(
×
893
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
894

895
              } else {
896
                interaction(
×
897
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
898
              }
899
            }
900
          }
901
        }
902
        boolean success = ensureLicenseAgreement(cmd);
4✔
903
        if (!success) {
2!
904
          return ValidationResultValid.get();
×
905
        }
906
        cmd.run();
2✔
907
      } finally {
908
        if (previousLogLevel != null) {
2!
909
          this.startContext.setLogLevel(previousLogLevel);
×
910
        }
911
      }
1✔
912
    } else {
913
      trace("Commandlet did not match");
×
914
    }
915
    return result;
2✔
916
  }
917

918
  private boolean ensureLicenseAgreement(Commandlet cmd) {
919

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

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

979
  private void verifyIdeRoot() {
980

981
    if (!isTest()) {
3!
982
      if (this.ideRoot == null) {
×
983
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
984
      } else if (this.ideHome != null) {
×
985
        Path ideRootPath = getIdeRootPathFromEnv();
×
986
        if (!this.ideRoot.equals(ideRootPath)) {
×
987
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath,
×
988
              this.ideHome.getFileName(), this.ideRoot);
×
989
        }
990
      }
991
    }
992
  }
1✔
993

994
  /**
995
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
996
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
997
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
998
   */
999
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1000

1001
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1002
    if (arguments.current().isStart()) {
4✔
1003
      arguments.next();
3✔
1004
    }
1005
    if (includeContextOptions) {
2✔
1006
      ContextCommandlet cc = new ContextCommandlet();
4✔
1007
      for (Property<?> property : cc.getProperties()) {
11✔
1008
        assert (property.isOption());
4!
1009
        property.apply(arguments, this, cc, collector);
7✔
1010
      }
1✔
1011
    }
1012
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1013
    CliArgument current = arguments.current();
3✔
1014
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1015
      collector.add(current.get(), null, null, null);
7✔
1016
    }
1017
    arguments.next();
3✔
1018
    while (commandletIterator.hasNext()) {
3✔
1019
      Commandlet cmd = commandletIterator.next();
4✔
1020
      if (!arguments.current().isEnd()) {
4✔
1021
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1022
      }
1023
    }
1✔
1024
    return collector.getSortedCandidates();
3✔
1025
  }
1026

1027
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1028

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

1085
  /**
1086
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1087
   *     {@link CliArguments#copy() copy} as needed.
1088
   * @param cmd the potential {@link Commandlet} to match.
1089
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1090
   */
1091
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1092

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

1139
  @Override
1140
  public String findBash() {
1141

1142
    String bash = "bash";
2✔
1143
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1144
      bash = findBashOnWindows();
×
1145
    }
1146

1147
    return bash;
2✔
1148
  }
1149

1150
  private String findBashOnWindows() {
1151

1152
    // Check if Git Bash exists in the default location
1153
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1154
    if (Files.exists(defaultPath)) {
×
1155
      return defaultPath.toString();
×
1156
    }
1157

1158
    // If not found in the default location, try the registry query
1159
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1160
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1161
    String regQueryResult;
1162
    for (String bashVariant : bashVariants) {
×
1163
      for (String registryKey : registryKeys) {
×
1164
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1165
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1166

1167
        try {
1168
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1169
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1170
            StringBuilder output = new StringBuilder();
×
1171
            String line;
1172

1173
            while ((line = reader.readLine()) != null) {
×
1174
              output.append(line);
×
1175
            }
1176

1177
            int exitCode = process.waitFor();
×
1178
            if (exitCode != 0) {
×
1179
              return null;
×
1180
            }
1181

1182
            regQueryResult = output.toString();
×
1183
            if (regQueryResult != null) {
×
1184
              int index = regQueryResult.indexOf("REG_SZ");
×
1185
              if (index != -1) {
×
1186
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1187
                return path + "\\bin\\bash.exe";
×
1188
              }
1189
            }
1190

1191
          }
×
1192
        } catch (Exception e) {
×
1193
          return null;
×
1194
        }
×
1195
      }
1196
    }
1197
    // no bash found
1198
    return null;
×
1199
  }
1200

1201
  @Override
1202
  public WindowsPathSyntax getPathSyntax() {
1203

1204
    return this.pathSyntax;
3✔
1205
  }
1206

1207
  /**
1208
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1209
   */
1210
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1211

1212
    this.pathSyntax = pathSyntax;
3✔
1213
  }
1✔
1214

1215
  /**
1216
   * @return the {@link IdeStartContextImpl}.
1217
   */
1218
  public IdeStartContextImpl getStartContext() {
1219

1220
    return startContext;
3✔
1221
  }
1222

1223
  /**
1224
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1225
   */
1226
  public void reload() {
1227

1228
    this.variables = null;
3✔
1229
    this.customToolRepository = null;
3✔
1230
  }
1✔
1231
}
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