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

devonfw / IDEasy / 12762884173

14 Jan 2025 07:19AM UTC coverage: 67.523% (-0.02%) from 67.541%
12762884173

Pull #918

github

web-flow
Merge f0caeba16 into 875fbff84
Pull Request #918: #915: fix custom tools

2576 of 4160 branches covered (61.92%)

Branch coverage included in aggregate %.

6672 of 9536 relevant lines covered (69.97%)

3.08 hits per line

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

63.49
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.util.ArrayList;
10
import java.util.HashMap;
11
import java.util.Iterator;
12
import java.util.List;
13
import java.util.Locale;
14
import java.util.Map;
15

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

63
/**
64
 * Abstract base implementation of {@link IdeContext}.
65
 */
66
public abstract class AbstractIdeContext implements IdeContext {
67

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

70
  private final IdeStartContextImpl startContext;
71

72
  private Path ideHome;
73

74
  private final Path ideRoot;
75

76
  private Path confPath;
77

78
  protected Path settingsPath;
79

80
  private Path softwarePath;
81

82
  private Path softwareExtraPath;
83

84
  private Path softwareRepositoryPath;
85

86
  protected Path pluginsPath;
87

88
  private Path workspacePath;
89

90
  private String workspaceName;
91

92
  protected Path urlsPath;
93

94
  private Path tempPath;
95

96
  private Path tempDownloadPath;
97

98
  private Path cwd;
99

100
  private Path downloadPath;
101

102
  private Path toolRepositoryPath;
103

104
  protected Path userHome;
105

106
  private Path userHomeIde;
107

108
  private SystemPath path;
109

110
  private WindowsPathSyntax pathSyntax;
111

112
  private final SystemInfo systemInfo;
113

114
  private EnvironmentVariables variables;
115

116
  private final FileAccess fileAccess;
117

118
  protected CommandletManager commandletManager;
119

120
  protected ToolRepository defaultToolRepository;
121

122
  private CustomToolRepository customToolRepository;
123

124
  private DirectoryMerger workspaceMerger;
125

126
  protected UrlMetadata urlMetadata;
127

128
  protected Path defaultExecutionDirectory;
129

130
  private StepImpl currentStep;
131

132
  protected Boolean online;
133

134
  protected IdeSystem system;
135

136
  private NetworkProxy networkProxy;
137

138
  /**
139
   * The constructor.
140
   *
141
   * @param startContext the {@link IdeLogger}.
142
   * @param workingDirectory the optional {@link Path} to current working directory.
143
   */
144
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
145

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

177
    // detection completed, initializing variables
178
    this.ideRoot = findIdeRoot(currentDir);
5✔
179

180
    setCwd(workingDirectory, workspace, currentDir);
5✔
181

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

202
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
203
  }
1✔
204

205
  private Path findIdeRoot(Path ideHomePath) {
206

207
    Path ideRootPath = null;
2✔
208
    if (ideHomePath != null) {
2✔
209
      ideRootPath = ideHomePath.getParent();
4✔
210
    } else if (!isTest()) {
3!
211
      ideRootPath = getIdeRootPathFromEnv();
×
212
    }
213
    return ideRootPath;
2✔
214
  }
215

216
  private Path getIdeRootPathFromEnv() {
217
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
218
    if (root != null) {
×
219
      Path rootPath = Path.of(root);
×
220
      if (Files.isDirectory(rootPath)) {
×
221
        return rootPath;
×
222
      }
223
    }
224
    return null;
×
225
  }
226

227
  @Override
228
  public void setCwd(Path userDir, String workspace, Path ideHome) {
229

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

261
    this.path = computeSystemPath();
4✔
262
  }
1✔
263

264
  private String getMessageIdeHomeFound() {
265

266
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
6✔
267
  }
268

269
  private String getMessageIdeHomeNotFound() {
270

271
    return "You are not inside an IDE installation: " + this.cwd;
4✔
272
  }
273

274
  private String getMessageIdeRootNotFound() {
275
    String root = getSystem().getEnv("IDE_ROOT");
×
276
    if (root == null) {
×
277
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
278
    } else {
279
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
280
    }
281
  }
282

283
  /**
284
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
285
   */
286
  public boolean isTest() {
287

288
    return false;
×
289
  }
290

291
  protected SystemPath computeSystemPath() {
292

293
    return new SystemPath(this);
×
294
  }
295

296

297
  private boolean isIdeHome(Path dir) {
298

299
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
300
      return false;
2✔
301
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
302
      return false;
×
303
    }
304
    return true;
2✔
305
  }
306

307
  private EnvironmentVariables createVariables() {
308

309
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
310
    AbstractEnvironmentVariables user = extendVariables(system, this.userHomeIde, EnvironmentVariablesType.USER);
7✔
311
    AbstractEnvironmentVariables settings = extendVariables(user, this.settingsPath, EnvironmentVariablesType.SETTINGS);
7✔
312
    // TODO should we keep this workspace properties? Was this feature ever used?
313
    AbstractEnvironmentVariables workspace = extendVariables(settings, this.workspacePath, EnvironmentVariablesType.WORKSPACE);
7✔
314
    AbstractEnvironmentVariables conf = extendVariables(workspace, this.confPath, EnvironmentVariablesType.CONF);
7✔
315
    return conf.resolved();
3✔
316
  }
317

318
  protected AbstractEnvironmentVariables createSystemVariables() {
319

320
    return EnvironmentVariables.ofSystem(this);
3✔
321
  }
322

323
  protected AbstractEnvironmentVariables extendVariables(AbstractEnvironmentVariables envVariables, Path propertiesPath, EnvironmentVariablesType type) {
324

325
    Path propertiesFile = null;
2✔
326
    if (propertiesPath == null) {
2✔
327
      trace("Configuration directory for type {} does not exist.", type);
10✔
328
    } else if (Files.isDirectory(propertiesPath)) {
5✔
329
      propertiesFile = propertiesPath.resolve(EnvironmentVariables.DEFAULT_PROPERTIES);
4✔
330
      boolean legacySupport = (type != EnvironmentVariablesType.USER);
7✔
331
      if (legacySupport && !Files.exists(propertiesFile)) {
7✔
332
        Path legacyFile = propertiesPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES);
4✔
333
        if (Files.exists(legacyFile)) {
5!
334
          propertiesFile = legacyFile;
×
335
        }
336
      }
337
    } else {
1✔
338
      debug("Configuration directory {} does not exist.", propertiesPath);
9✔
339
    }
340
    return envVariables.extend(propertiesFile, type);
5✔
341
  }
342

343
  @Override
344
  public SystemInfo getSystemInfo() {
345

346
    return this.systemInfo;
3✔
347
  }
348

349
  @Override
350
  public FileAccess getFileAccess() {
351

352
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
353
    configureNetworkProxy();
2✔
354
    return this.fileAccess;
3✔
355
  }
356

357
  @Override
358
  public CommandletManager getCommandletManager() {
359

360
    return this.commandletManager;
3✔
361
  }
362

363
  @Override
364
  public ToolRepository getDefaultToolRepository() {
365

366
    return this.defaultToolRepository;
3✔
367
  }
368

369
  @Override
370
  public CustomToolRepository getCustomToolRepository() {
371

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

378
  @Override
379
  public Path getIdeHome() {
380

381
    return this.ideHome;
3✔
382
  }
383

384
  @Override
385
  public String getProjectName() {
386

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

393
  @Override
394
  public Path getIdeRoot() {
395

396
    return this.ideRoot;
3✔
397
  }
398

399
  @Override
400
  public Path getCwd() {
401

402
    return this.cwd;
3✔
403
  }
404

405
  @Override
406
  public Path getTempPath() {
407

408
    return this.tempPath;
3✔
409
  }
410

411
  @Override
412
  public Path getTempDownloadPath() {
413

414
    return this.tempDownloadPath;
3✔
415
  }
416

417
  @Override
418
  public Path getUserHome() {
419

420
    return this.userHome;
3✔
421
  }
422

423
  @Override
424
  public Path getUserHomeIde() {
425

426
    return this.userHomeIde;
3✔
427
  }
428

429
  @Override
430
  public Path getSettingsPath() {
431

432
    return this.settingsPath;
3✔
433
  }
434

435
  @Override
436
  public Path getConfPath() {
437

438
    return this.confPath;
3✔
439
  }
440

441
  @Override
442
  public Path getSoftwarePath() {
443

444
    return this.softwarePath;
3✔
445
  }
446

447
  @Override
448
  public Path getSoftwareExtraPath() {
449

450
    return this.softwareExtraPath;
3✔
451
  }
452

453
  @Override
454
  public Path getSoftwareRepositoryPath() {
455

456
    return this.softwareRepositoryPath;
3✔
457
  }
458

459
  @Override
460
  public Path getPluginsPath() {
461

462
    return this.pluginsPath;
3✔
463
  }
464

465
  @Override
466
  public String getWorkspaceName() {
467

468
    return this.workspaceName;
3✔
469
  }
470

471
  @Override
472
  public Path getWorkspacePath() {
473

474
    return this.workspacePath;
3✔
475
  }
476

477
  @Override
478
  public Path getDownloadPath() {
479

480
    return this.downloadPath;
3✔
481
  }
482

483
  @Override
484
  public Path getUrlsPath() {
485

486
    return this.urlsPath;
3✔
487
  }
488

489
  @Override
490
  public Path getToolRepositoryPath() {
491

492
    return this.toolRepositoryPath;
3✔
493
  }
494

495
  @Override
496
  public SystemPath getPath() {
497

498
    return this.path;
3✔
499
  }
500

501
  @Override
502
  public EnvironmentVariables getVariables() {
503

504
    if (this.variables == null) {
3✔
505
      this.variables = createVariables();
4✔
506
    }
507
    return this.variables;
3✔
508
  }
509

510
  @Override
511
  public UrlMetadata getUrls() {
512

513
    if (this.urlMetadata == null) {
3✔
514
      if (!isTest()) {
3!
515
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, this.urlsPath, null);
×
516
      }
517
      this.urlMetadata = new UrlMetadata(this);
6✔
518
    }
519
    return this.urlMetadata;
3✔
520
  }
521

522
  @Override
523
  public boolean isQuietMode() {
524

525
    return this.startContext.isQuietMode();
4✔
526
  }
527

528
  @Override
529
  public boolean isBatchMode() {
530

531
    return this.startContext.isBatchMode();
×
532
  }
533

534
  @Override
535
  public boolean isForceMode() {
536

537
    return this.startContext.isForceMode();
4✔
538
  }
539

540
  @Override
541
  public boolean isOfflineMode() {
542

543
    return this.startContext.isOfflineMode();
4✔
544
  }
545

546
  @Override
547
  public boolean isSkipUpdatesMode() {
548
    return this.startContext.isSkipUpdatesMode();
×
549
  }
550

551
  @Override
552
  public boolean isOnline() {
553

554
    if (this.online == null) {
3✔
555
      configureNetworkProxy();
2✔
556
      // we currently assume we have only a CLI process that runs shortly
557
      // therefore we run this check only once to save resources when this method is called many times
558
      try {
559
        int timeout = 1000;
2✔
560
        //open a connection to github.com and try to retrieve data
561
        //getContent fails if there is no connection
562
        URLConnection connection = new URL("https://www.github.com").openConnection();
6✔
563
        connection.setConnectTimeout(timeout);
3✔
564
        connection.getContent();
3✔
565
        this.online = Boolean.TRUE;
3✔
566
      } catch (Exception ignored) {
×
567
        this.online = Boolean.FALSE;
×
568
      }
1✔
569
    }
570
    return this.online.booleanValue();
4✔
571
  }
572

573
  private void configureNetworkProxy() {
574
    if (this.networkProxy == null) {
3✔
575
      this.networkProxy = new NetworkProxy(this);
6✔
576
      this.networkProxy.configure();
3✔
577
    }
578
  }
1✔
579

580
  @Override
581
  public Locale getLocale() {
582

583
    Locale locale = this.startContext.getLocale();
4✔
584
    if (locale == null) {
2!
585
      locale = Locale.getDefault();
×
586
    }
587
    return locale;
2✔
588
  }
589

590
  @Override
591
  public DirectoryMerger getWorkspaceMerger() {
592

593
    if (this.workspaceMerger == null) {
3✔
594
      this.workspaceMerger = new DirectoryMerger(this);
6✔
595
    }
596
    return this.workspaceMerger;
3✔
597
  }
598

599
  /**
600
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
601
   */
602
  @Override
603
  public Path getDefaultExecutionDirectory() {
604

605
    return this.defaultExecutionDirectory;
×
606
  }
607

608
  /**
609
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
610
   */
611
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
612

613
    if (defaultExecutionDirectory != null) {
×
614
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
615
    }
616
  }
×
617

618
  @Override
619
  public GitContext getGitContext() {
620

621
    return new GitContextImpl(this);
×
622
  }
623

624
  @Override
625
  public ProcessContext newProcess() {
626

627
    ProcessContext processContext = createProcessContext();
3✔
628
    if (this.defaultExecutionDirectory != null) {
3!
629
      processContext.directory(this.defaultExecutionDirectory);
×
630
    }
631
    return processContext;
2✔
632
  }
633

634
  @Override
635
  public IdeSystem getSystem() {
636

637
    if (this.system == null) {
×
638
      this.system = new IdeSystemImpl(this);
×
639
    }
640
    return this.system;
×
641
  }
642

643
  /**
644
   * @return a new instance of {@link ProcessContext}.
645
   * @see #newProcess()
646
   */
647
  protected ProcessContext createProcessContext() {
648

649
    return new ProcessContextImpl(this);
×
650
  }
651

652
  @Override
653
  public IdeSubLogger level(IdeLogLevel level) {
654

655
    return this.startContext.level(level);
5✔
656
  }
657

658
  @Override
659
  public void logIdeHomeAndRootStatus() {
660

661
    if (this.ideRoot != null) {
3!
662
      success("IDE_ROOT is set to {}", this.ideRoot);
×
663
    }
664
    if (this.ideHome == null) {
3!
665
      warning(getMessageIdeHomeNotFound());
5✔
666
    } else {
667
      success("IDE_HOME is set to {}", this.ideHome);
×
668
    }
669
  }
1✔
670

671
  @Override
672
  public String askForInput(String message, String defaultValue) {
673

674
    if (!message.isBlank()) {
×
675
      info(message);
×
676
    }
677
    if (isBatchMode()) {
×
678
      if (isForceMode()) {
×
679
        return defaultValue;
×
680
      } else {
681
        throw new CliAbortException();
×
682
      }
683
    }
684
    String input = readLine().trim();
×
685
    return input.isEmpty() ? defaultValue : input;
×
686
  }
687

688
  @Override
689
  public String askForInput(String message) {
690

691
    String input;
692
    do {
693
      info(message);
3✔
694
      input = readLine().trim();
4✔
695
    } while (input.isEmpty());
3!
696

697
    return input;
2✔
698
  }
699

700
  @SuppressWarnings("unchecked")
701
  @Override
702
  public <O> O question(String question, O... options) {
703

704
    assert (options.length >= 2);
×
705
    interaction(question);
×
706
    Map<String, O> mapping = new HashMap<>(options.length);
×
707
    int i = 0;
×
708
    for (O option : options) {
×
709
      i++;
×
710
      String key = "" + option;
×
711
      addMapping(mapping, key, option);
×
712
      String numericKey = Integer.toString(i);
×
713
      if (numericKey.equals(key)) {
×
714
        trace("Options should not be numeric: " + key);
×
715
      } else {
716
        addMapping(mapping, numericKey, option);
×
717
      }
718
      interaction("Option " + numericKey + ": " + key);
×
719
    }
720
    O option = null;
×
721
    if (isBatchMode()) {
×
722
      if (isForceMode()) {
×
723
        option = options[0];
×
724
        interaction("" + option);
×
725
      }
726
    } else {
727
      while (option == null) {
×
728
        String answer = readLine();
×
729
        option = mapping.get(answer);
×
730
        if (option == null) {
×
731
          warning("Invalid answer: '" + answer + "' - please try again.");
×
732
        }
733
      }
×
734
    }
735
    return option;
×
736
  }
737

738
  /**
739
   * @return the input from the end-user (e.g. read from the console).
740
   */
741
  protected abstract String readLine();
742

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

745
    O duplicate = mapping.put(key, option);
×
746
    if (duplicate != null) {
×
747
      throw new IllegalArgumentException("Duplicated option " + key);
×
748
    }
749
  }
×
750

751
  @Override
752
  public Step getCurrentStep() {
753

754
    return this.currentStep;
×
755
  }
756

757
  @Override
758
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
759

760
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
761
    return this.currentStep;
3✔
762
  }
763

764
  /**
765
   * Internal method to end the running {@link Step}.
766
   *
767
   * @param step the current {@link Step} to end.
768
   */
769
  public void endStep(StepImpl step) {
770

771
    if (step == this.currentStep) {
4!
772
      this.currentStep = this.currentStep.getParent();
6✔
773
    } else {
774
      String currentStepName = "null";
×
775
      if (this.currentStep != null) {
×
776
        currentStepName = this.currentStep.getName();
×
777
      }
778
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
779
    }
780
  }
1✔
781

782
  /**
783
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
784
   *
785
   * @param arguments the {@link CliArgument}.
786
   * @return the return code of the execution.
787
   */
788
  public int run(CliArguments arguments) {
789

790
    CliArgument current = arguments.current();
3✔
791
    assert (this.currentStep == null);
4!
792
    boolean supressStepSuccess = false;
2✔
793
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
794
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
795
    Commandlet cmd = null;
2✔
796
    ValidationResult result = null;
2✔
797
    try {
798
      while (commandletIterator.hasNext()) {
3!
799
        cmd = commandletIterator.next();
4✔
800
        result = applyAndRun(arguments.copy(), cmd);
6✔
801
        if (result.isValid()) {
3!
802
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
803
          step.success();
2✔
804
          return ProcessResult.SUCCESS;
4✔
805
        }
806
      }
807
      this.startContext.activateLogging();
×
808
      if (result != null) {
×
809
        error(result.getErrorMessage());
×
810
      }
811
      step.error("Invalid arguments: {}", current.getArgs());
×
812
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
×
813
      if (cmd != null) {
×
814
        help.commandlet.setValue(cmd);
×
815
      }
816
      help.run();
×
817
      return 1;
×
818
    } catch (Throwable t) {
×
819
      this.startContext.activateLogging();
×
820
      step.error(t, true);
×
821
      throw t;
×
822
    } finally {
823
      step.close();
2✔
824
      assert (this.currentStep == null);
4!
825
      step.logSummary(supressStepSuccess);
3✔
826
    }
827
  }
828

829
  /**
830
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
831
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
832
   *     {@link Commandlet} did not match and we have to try a different candidate).
833
   */
834
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
835

836
    IdeLogLevel previousLogLevel = null;
2✔
837
    cmd.reset();
2✔
838
    ValidationResult result = apply(arguments, cmd);
5✔
839
    if (result.isValid()) {
3!
840
      result = cmd.validate();
3✔
841
    }
842
    if (result.isValid()) {
3!
843
      debug("Running commandlet {}", cmd);
9✔
844
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
845
        throw new CliException(getMessageIdeHomeNotFound(), ProcessResult.NO_IDE_HOME);
×
846
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
847
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
×
848
      }
849
      try {
850
        if (cmd.isProcessableOutput()) {
3!
851
          if (!debug().isEnabled()) {
×
852
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
853
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
854
          }
855
          this.startContext.activateLogging();
×
856
        } else {
857
          this.startContext.activateLogging();
3✔
858
          verifyIdeRoot();
2✔
859
          if (cmd.isIdeHomeRequired()) {
3✔
860
            debug(getMessageIdeHomeFound());
4✔
861
          }
862
          if (this.settingsPath != null) {
3✔
863
            if (getGitContext().isRepositoryUpdateAvailable(this.settingsPath) ||
7!
864
                (getGitContext().fetchIfNeeded(this.settingsPath) && getGitContext().isRepositoryUpdateAvailable(this.settingsPath))) {
5!
865
              interaction("Updates are available for the settings repository. If you want to pull the latest changes, call ide update.");
×
866
            }
867
          }
868
        }
869
        cmd.run();
2✔
870
      } finally {
871
        if (previousLogLevel != null) {
2!
872
          this.startContext.setLogLevel(previousLogLevel);
×
873
        }
874
      }
1✔
875
    } else {
876
      trace("Commandlet did not match");
×
877
    }
878
    return result;
2✔
879
  }
880

881
  private void verifyIdeRoot() {
882
    if (!isTest()) {
3!
883
      if (this.ideRoot == null) {
×
884
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
885
      } else if (this.ideHome != null) {
×
886
        Path ideRootPath = getIdeRootPathFromEnv();
×
887
        if (!this.ideRoot.equals(ideRootPath)) {
×
888
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath,
×
889
              this.ideHome.getFileName(), this.ideRoot);
×
890
        }
891
      }
892
    }
893
  }
1✔
894

895
  /**
896
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
897
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
898
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
899
   */
900
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
901
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
902
    if (arguments.current().isStart()) {
4✔
903
      arguments.next();
3✔
904
    }
905
    if (includeContextOptions) {
2✔
906
      ContextCommandlet cc = new ContextCommandlet();
4✔
907
      for (Property<?> property : cc.getProperties()) {
11✔
908
        assert (property.isOption());
4!
909
        property.apply(arguments, this, cc, collector);
7✔
910
      }
1✔
911
    }
912
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
913
    CliArgument current = arguments.current();
3✔
914
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
915
      collector.add(current.get(), null, null, null);
7✔
916
    }
917
    arguments.next();
3✔
918
    while (commandletIterator.hasNext()) {
3✔
919
      Commandlet cmd = commandletIterator.next();
4✔
920
      if (!arguments.current().isEnd()) {
4✔
921
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
922
      }
923
    }
1✔
924
    return collector.getSortedCandidates();
3✔
925
  }
926

927
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
928
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
929
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
930
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
931
    List<Property<?>> properties = cmd.getProperties();
3✔
932
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
933
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
934
    for (Property<?> property : properties) {
10✔
935
      if (property.isOption()) {
3✔
936
        optionProperties.add(property);
4✔
937
      }
938
    }
1✔
939
    CliArgument currentArgument = arguments.current();
3✔
940
    while (!currentArgument.isEnd()) {
3✔
941
      trace("Trying to match argument '{}'", currentArgument);
9✔
942
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
943
        if (currentArgument.isCompletion()) {
3✔
944
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
945
          while (optionIterator.hasNext()) {
3✔
946
            Property<?> option = optionIterator.next();
4✔
947
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
948
            if (success) {
2✔
949
              optionIterator.remove();
2✔
950
              arguments.next();
3✔
951
            }
952
          }
1✔
953
        } else {
1✔
954
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
955
          if (option != null) {
2✔
956
            arguments.next();
3✔
957
            boolean removed = optionProperties.remove(option);
4✔
958
            if (!removed) {
2!
959
              option = null;
×
960
            }
961
          }
962
          if (option == null) {
2✔
963
            trace("No such option was found.");
3✔
964
            return;
1✔
965
          }
966
        }
1✔
967
      } else {
968
        if (valueIterator.hasNext()) {
3✔
969
          Property<?> valueProperty = valueIterator.next();
4✔
970
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
971
          if (!success) {
2!
972
            trace("Completion cannot match any further.");
×
973
            return;
×
974
          }
975
        } else {
1✔
976
          trace("No value left for completion.");
3✔
977
          return;
1✔
978
        }
979
      }
980
      currentArgument = arguments.current();
4✔
981
    }
982
  }
1✔
983

984

985
  /**
986
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
987
   *     {@link CliArguments#copy() copy} as needed.
988
   * @param cmd the potential {@link Commandlet} to match.
989
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
990
   */
991
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
992

993
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
994
    CliArgument currentArgument = arguments.current();
3✔
995
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
996
    Property<?> property = null;
2✔
997
    if (propertyIterator.hasNext()) {
3!
998
      property = propertyIterator.next();
4✔
999
    }
1000
    while (!currentArgument.isEnd()) {
3✔
1001
      trace("Trying to match argument '{}'", currentArgument);
9✔
1002
      Property<?> currentProperty = property;
2✔
1003
      if (!arguments.isEndOptions()) {
3!
1004
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1005
        if (option != null) {
2!
1006
          currentProperty = option;
×
1007
        }
1008
      }
1009
      if (currentProperty == null) {
2!
1010
        trace("No option or next value found");
×
1011
        ValidationState state = new ValidationState(null);
×
1012
        state.addErrorMessage("No matching property found");
×
1013
        return state;
×
1014
      }
1015
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1016
      if (currentProperty == property) {
3!
1017
        if (!property.isMultiValued()) {
3✔
1018
          if (propertyIterator.hasNext()) {
3✔
1019
            property = propertyIterator.next();
5✔
1020
          } else {
1021
            property = null;
2✔
1022
          }
1023
        }
1024
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1025
          arguments.stopSplitShortOptions();
2✔
1026
        }
1027
      }
1028
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1029
      if (!matches && currentArgument.isCompletion()) {
2!
1030
        ValidationState state = new ValidationState(null);
×
1031
        state.addErrorMessage("No matching property found");
×
1032
        return state;
×
1033
      }
1034
      currentArgument = arguments.current();
3✔
1035
    }
1✔
1036
    return ValidationResultValid.get();
2✔
1037
  }
1038

1039
  @Override
1040
  public String findBash() {
1041

1042
    String bash = "bash";
2✔
1043
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1044
      bash = findBashOnWindows();
×
1045
    }
1046

1047
    return bash;
2✔
1048
  }
1049

1050
  private String findBashOnWindows() {
1051

1052
    // Check if Git Bash exists in the default location
1053
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1054
    if (Files.exists(defaultPath)) {
×
1055
      return defaultPath.toString();
×
1056
    }
1057

1058
    // If not found in the default location, try the registry query
1059
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1060
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1061
    String regQueryResult;
1062
    for (String bashVariant : bashVariants) {
×
1063
      for (String registryKey : registryKeys) {
×
1064
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1065
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1066

1067
        try {
1068
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1069
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1070
            StringBuilder output = new StringBuilder();
×
1071
            String line;
1072

1073
            while ((line = reader.readLine()) != null) {
×
1074
              output.append(line);
×
1075
            }
1076

1077
            int exitCode = process.waitFor();
×
1078
            if (exitCode != 0) {
×
1079
              return null;
×
1080
            }
1081

1082
            regQueryResult = output.toString();
×
1083
            if (regQueryResult != null) {
×
1084
              int index = regQueryResult.indexOf("REG_SZ");
×
1085
              if (index != -1) {
×
1086
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1087
                return path + "\\bin\\bash.exe";
×
1088
              }
1089
            }
1090

1091
          }
×
1092
        } catch (Exception e) {
×
1093
          return null;
×
1094
        }
×
1095
      }
1096
    }
1097
    // no bash found
1098
    return null;
×
1099
  }
1100

1101
  @Override
1102
  public WindowsPathSyntax getPathSyntax() {
1103
    return this.pathSyntax;
3✔
1104
  }
1105

1106
  /**
1107
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1108
   */
1109
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1110

1111
    this.pathSyntax = pathSyntax;
3✔
1112
  }
1✔
1113

1114
  /**
1115
   * @return the {@link IdeStartContextImpl}.
1116
   */
1117
  public IdeStartContextImpl getStartContext() {
1118

1119
    return startContext;
3✔
1120
  }
1121

1122
  /**
1123
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1124
   */
1125
  public void reload() {
1126
    this.variables = null;
3✔
1127
    this.customToolRepository = null;
3✔
1128
  }
1✔
1129
}
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