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

devonfw / IDEasy / 27742596455

18 Jun 2026 07:02AM UTC coverage: 71.299% (+0.02%) from 71.279%
27742596455

push

github

web-flow
#1392: smart completions (#1999)

Co-authored-by: Adem Zarrouki <81715160+AdemZarrouki@users.noreply.github.com>
Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>
Co-authored-by: T9 <103292348+shodiBoy1@users.noreply.github.com>

4681 of 7268 branches covered (64.41%)

Branch coverage included in aggregate %.

12097 of 16264 relevant lines covered (74.38%)

3.15 hits per line

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

84.69
cli/src/main/java/com/devonfw/tools/ide/tool/mvn/Mvn.java
1
package com.devonfw.tools.ide.tool.mvn;
2

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.security.SecureRandom;
6
import java.util.Base64;
7
import java.util.LinkedHashSet;
8
import java.util.List;
9
import java.util.Objects;
10
import java.util.Set;
11
import java.util.regex.Matcher;
12

13
import org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15

16
import com.devonfw.tools.ide.common.Tag;
17
import com.devonfw.tools.ide.completion.AutoCompletionRegistry;
18
import com.devonfw.tools.ide.context.IdeContext;
19
import com.devonfw.tools.ide.git.GitContext;
20
import com.devonfw.tools.ide.io.FileAccess;
21
import com.devonfw.tools.ide.process.ProcessContext;
22
import com.devonfw.tools.ide.process.ProcessMode;
23
import com.devonfw.tools.ide.process.ProcessResult;
24
import com.devonfw.tools.ide.step.Step;
25
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
26
import com.devonfw.tools.ide.tool.ToolCommandlet;
27
import com.devonfw.tools.ide.tool.ToolInstallRequest;
28
import com.devonfw.tools.ide.variable.IdeVariables;
29
import com.devonfw.tools.ide.variable.VariableSyntax;
30

31
/**
32
 * {@link ToolCommandlet} for <a href="https://maven.apache.org/">maven</a>.
33
 */
34
public class Mvn extends LocalToolCommandlet {
35

36
  private static final Logger LOG = LoggerFactory.getLogger(Mvn.class);
3✔
37

38
  /** The name of the mvn folder. */
39
  public static final String MVN_CONFIG_FOLDER = "mvn";
40

41
  /** The name of the m2 repository. */
42
  public static final String MVN_CONFIG_LEGACY_FOLDER = ".m2";
43

44
  /** The name of the settings-security.xml */
45
  public static final String SETTINGS_SECURITY_FILE = "settings-security.xml";
46

47
  /** The mvn wrapper file name. */
48
  public static final String MVN_WRAPPER_FILENAME = "mvnw";
49

50
  /** The pom.xml file name. */
51
  public static final String POM_XML = "pom.xml";
52

53
  /** The name of the file settings.xml. */
54
  public static final String SETTINGS_FILE = "settings.xml";
55

56
  private static final String DOCUMENTATION_PAGE_CONF = "https://github.com/devonfw/IDEasy/blob/main/documentation/conf.adoc";
57

58
  private static final String ERROR_SETTINGS_FILE_MESSAGE =
59
      "Failed to create settings file at: {}. For further details see:\n" + DOCUMENTATION_PAGE_CONF;
60

61
  private static final String ERROR_SETTINGS_SECURITY_FILE_MESSAGE =
62
      "Failed to create settings security file at: {}. For further details see:\n" + DOCUMENTATION_PAGE_CONF;
63

64
  private static final VariableSyntax VARIABLE_SYNTAX = VariableSyntax.SQUARE;
3✔
65

66
  /**
67
   * The constructor.
68
   *
69
   * @param context the {@link IdeContext}.
70
   */
71
  public Mvn(IdeContext context) {
72

73
    super(context, "mvn", Set.of(Tag.JAVA, Tag.BUILD));
7✔
74
  }
1✔
75

76
  /**
77
   * Initializes Maven-specific auto-completion candidates.
78
   *
79
   * @param registry the {@link AutoCompletionRegistry} to initialize.
80
   */
81
  @Override
82
  protected void initAutoCompletionRegistry(AutoCompletionRegistry registry) {
83

84
    registry.add("clean");
3✔
85
    registry.add("package");
3✔
86
    registry.add("install");
3✔
87
    registry.add("deploy");
3✔
88
    registry.add("test");
3✔
89
    registry.add("verify");
3✔
90
    registry.add("validate");
3✔
91
    registry.add("compile");
3✔
92
    registry.add("dependency:tree");
3✔
93
    registry.add("dependency:list");
3✔
94
    registry.add("help:effective-settings");
3✔
95
    registry.add("-DskipTests");
3✔
96
    registry.add("exec:java");
3✔
97
    registry.add("-Dexec.mainClass=");
3✔
98
    registry.add("-Dexec.args=");
3✔
99
    registry.add("--also-make");
3✔
100
    registry.add("--also-make-dependents");
3✔
101
    registry.add("--fail-at-end");
3✔
102
    registry.add("--fail-fast");
3✔
103
    registry.add("-T1C");
3✔
104
    registry.add("-DdeployAtEnd=true");
3✔
105
  }
1✔
106

107
  @Override
108
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode) {
109
    Path mvn = Path.of(getBinaryName());
6✔
110
    Path wrapper = findWrapper(MVN_WRAPPER_FILENAME);
4✔
111
    pc.executable(Objects.requireNonNullElse(wrapper, mvn));
7✔
112
  }
1✔
113

114
  @Override
115
  protected void postInstallOnNewInstallation(ToolInstallRequest request) {
116

117
    super.postInstallOnNewInstallation(request);
3✔
118
    // locate templates...
119
    Path templatesConfMvnFolder = getMavenTemplatesFolder();
3✔
120
    if (templatesConfMvnFolder == null) {
2✔
121
      return;
1✔
122
    }
123
    // locate real config...
124
    boolean legacy = templatesConfMvnFolder.getFileName().toString().equals(MVN_CONFIG_LEGACY_FOLDER);
6✔
125
    Path mvnConfigPath = getMavenConfFolder(legacy);
4✔
126

127
    Path settingsSecurityFile = mvnConfigPath.resolve(SETTINGS_SECURITY_FILE);
4✔
128
    createSettingsSecurityFile(settingsSecurityFile);
3✔
129

130
    Path settingsFile = mvnConfigPath.resolve(SETTINGS_FILE);
4✔
131
    createSettingsFile(settingsFile, templatesConfMvnFolder.resolve(SETTINGS_FILE), settingsSecurityFile);
7✔
132
  }
1✔
133

134
  private void createSettingsSecurityFile(Path settingsSecurityFile) {
135

136
    if (Files.exists(settingsSecurityFile)) {
5!
137
      return; // file already exists, nothing to do...
×
138
    }
139
    Step step = this.context.newStep("Create mvn settings security file at " + settingsSecurityFile);
7✔
140
    step.run(() -> doCreateSettingsSecurityFileStep(settingsSecurityFile, step));
12✔
141
  }
1✔
142

143
  private void doCreateSettingsSecurityFileStep(Path settingsSecurityFile, Step step) {
144

145
    SecureRandom secureRandom = new SecureRandom();
4✔
146
    byte[] randomBytes = new byte[20];
3✔
147

148
    secureRandom.nextBytes(randomBytes);
3✔
149
    String base64String = Base64.getEncoder().encodeToString(randomBytes);
4✔
150

151
    String encryptedMasterPassword = retrievePassword("--encrypt-master-password", base64String);
5✔
152

153
    String settingsSecurityXml =
3✔
154
        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<settingsSecurity>\n" + "  <master>" + encryptedMasterPassword + "</master>\n"
155
            + "</settingsSecurity>";
156
    try {
157
      this.context.getFileAccess().writeFileContent(settingsSecurityXml, settingsSecurityFile, true);
7✔
158
    } catch (Exception e) {
×
159
      step.error(e, ERROR_SETTINGS_SECURITY_FILE_MESSAGE, settingsSecurityFile);
×
160
    }
1✔
161
  }
1✔
162

163
  private void createSettingsFile(Path settingsFile, Path settingsTemplateFile, Path settingsSecurityFile) {
164

165
    if (Files.exists(settingsFile)) {
5!
166
      return;
×
167
    }
168
    if (!Files.exists(settingsTemplateFile)) {
5!
169
      LOG.warn("Missing maven settings template at {}. ", settingsTemplateFile);
×
170
      return;
×
171
    }
172
    Step step = this.context.newStep("Create mvn settings file at " + settingsFile);
7✔
173
    step.run(() -> doCreateSettingsFile(settingsFile, settingsTemplateFile, settingsSecurityFile, step));
16✔
174
  }
1✔
175

176
  private void doCreateSettingsFile(Path settingsFile, Path settingsTemplateFile, Path settingsSecurityFile, Step step) {
177

178
    try {
179
      FileAccess fileAccess = this.context.getFileAccess();
4✔
180
      String content = fileAccess.readFileContent(settingsTemplateFile);
4✔
181
      GitContext gitContext = this.context.getGitContext();
4✔
182
      String gitSettingsUrl = gitContext.retrieveGitUrl(this.context.getSettingsPath());
6✔
183
      if (gitSettingsUrl == null) {
2!
184
        LOG.warn("Failed to determine git remote URL for settings folder.");
×
185
      } else if (!gitSettingsUrl.equals(GitContext.DEFAULT_SETTINGS_GIT_URL) && Files.exists(settingsSecurityFile)) {
9!
186
        Set<String> variables = findVariables(content);
4✔
187
        for (String variable : variables) {
10✔
188
          String secret = getEncryptedPassword(variable);
4✔
189
          content = content.replace(VARIABLE_SYNTAX.create(variable), secret);
7✔
190
        }
1✔
191
      }
192
      fileAccess.writeFileContent(content, settingsFile, true);
5✔
193
      step.success();
2✔
194
    } catch (Exception e) {
1✔
195
      step.error(e, ERROR_SETTINGS_FILE_MESSAGE, settingsFile);
10✔
196
    }
1✔
197
  }
1✔
198

199
  private String getEncryptedPassword(String variable) {
200

201
    String input = this.context.askForInput("Please enter secret value for variable " + variable + ":");
6✔
202
    String encryptedPassword = retrievePassword("--encrypt-password", input);
5✔
203
    LOG.info("Encrypted as {}", encryptedPassword);
4✔
204

205
    return encryptedPassword;
2✔
206
  }
207

208
  private String retrievePassword(String option, String input) {
209

210
    ProcessResult result = runTool(this.context.newProcess(), ProcessMode.DEFAULT_CAPTURE, List.of(option, input,
11✔
211
        getSettingsSecurityProperty()));
1✔
212

213
    return result.getSingleOutput(null);
4✔
214
  }
215

216
  private Set<String> findVariables(String content) {
217

218
    Set<String> variables = new LinkedHashSet<>();
4✔
219
    Matcher matcher = VARIABLE_SYNTAX.getPattern().matcher(content);
5✔
220
    while (matcher.find()) {
3✔
221
      String variableName = VARIABLE_SYNTAX.getVariable(matcher);
4✔
222
      variables.add(variableName);
4✔
223
    }
1✔
224
    return variables;
2✔
225
  }
226

227
  @Override
228
  public String getToolHelpArguments() {
229

230
    return "-h";
2✔
231
  }
232

233
  /**
234
   * @return the {@link Path} to the folder with the maven configuration templates.
235
   */
236
  public Path getMavenTemplatesFolder() {
237

238
    Path templatesFolder = this.context.getSettingsTemplatePath();
4✔
239
    if (templatesFolder == null) {
2✔
240
      return null;
2✔
241
    }
242
    Path templatesConfFolder = templatesFolder.resolve(IdeContext.FOLDER_CONF);
4✔
243
    Path templatesConfMvnFolder = templatesConfFolder.resolve(MVN_CONFIG_FOLDER);
4✔
244
    if (!Files.isDirectory(templatesConfMvnFolder)) {
5✔
245
      Path templatesConfMvnLegacyFolder = templatesConfFolder.resolve(MVN_CONFIG_LEGACY_FOLDER);
4✔
246
      if (!Files.isDirectory(templatesConfMvnLegacyFolder)) {
5!
247
        LOG.warn("No maven templates found neither in {} nor in {} - configuration broken", templatesConfMvnFolder,
5✔
248
            templatesConfMvnLegacyFolder);
249
        return null;
2✔
250
      }
251
      templatesConfMvnFolder = templatesConfMvnLegacyFolder;
×
252
    }
253
    return templatesConfMvnFolder;
2✔
254
  }
255

256
  /**
257
   * @param legacy - {@code true} to enforce legacy fallback creation, {@code false} otherwise.
258
   * @return the {@link Path} to the maven configuration folder (where "settings.xml" can be found).
259
   */
260
  public Path getMavenConfFolder(boolean legacy) {
261

262
    Path mvnConfigFolder = resolveMavenConfFolder(legacy);
4✔
263
    this.context.getFileAccess().mkdirs(mvnConfigFolder);
5✔
264
    return mvnConfigFolder;
2✔
265
  }
266

267
  /**
268
   * Helper method to resolve maven config folder without creating directories.
269
   *
270
   * @param legacy - {@code true} to prefer devonfw-ide legacy folder when neither exists, {@code false} to prefer new folder.
271
   * @return the {@link Path} to the maven configuration folder that should be used.
272
   */
273
  private Path resolveMavenConfFolder(boolean legacy) {
274
    Path confPath = this.context.getConfPath();
4✔
275
    Path mvnConfigFolder = confPath.resolve(MVN_CONFIG_FOLDER);
4✔
276
    if (!Files.isDirectory(mvnConfigFolder)) {
5✔
277
      Path mvnConfigLegacyFolder = confPath.resolve(Mvn.MVN_CONFIG_LEGACY_FOLDER);
4✔
278
      if (Files.isDirectory(mvnConfigLegacyFolder)) {
5!
279
        mvnConfigFolder = mvnConfigLegacyFolder;
×
280
      } else {
281
        if (legacy) {
2✔
282
          mvnConfigFolder = mvnConfigLegacyFolder;
2✔
283
        }
284
        // Note: directories are created by the caller if needed
285
      }
286
    }
287
    return mvnConfigFolder;
2✔
288
  }
289

290
  /**
291
   * @return the {@link Path} pointing to the maven configuration directory (where "settings.xml" or "settings-security.xml" are located). This method provides
292
   *     the same behavior as the original IdeContext.getMavenConfigurationFolder() method.
293
   */
294
  public Path getMavenConfigurationFolder() {
295
    Path confPath = this.context.getConfPath();
4✔
296
    if (confPath != null) {
2!
297
      Path mvnConfigFolder = resolveMavenConfFolder(true);
4✔
298
      // Only return the path if the directory actually exists
299
      if (Files.isDirectory(mvnConfigFolder)) {
5✔
300
        return mvnConfigFolder;
2✔
301
      }
302
    }
303
    // fallback to USER_HOME/.m2 folder
304
    return this.context.getUserHome().resolve(MVN_CONFIG_LEGACY_FOLDER);
6✔
305
  }
306

307
  /**
308
   * @return the maven arguments (MVN_ARGS).
309
   */
310
  public String getMavenArgs() {
311
    Path mavenConfFolder = getMavenConfFolder(false);
4✔
312
    Path mvnSettingsFile = mavenConfFolder.resolve(Mvn.SETTINGS_FILE);
4✔
313
    Path settingsSecurityFile = mavenConfFolder.resolve(SETTINGS_SECURITY_FILE);
4✔
314
    boolean settingsFileExists = Files.exists(mvnSettingsFile);
5✔
315
    boolean securityFileExists = Files.exists(settingsSecurityFile);
5✔
316
    if (!settingsFileExists && !securityFileExists) {
4✔
317
      return ""; // this clears the MAVEN_ARGS
2✔
318
    }
319
    StringBuilder sb = new StringBuilder();
4✔
320
    if (settingsFileExists) {
2✔
321
      sb.append("-s ");
4✔
322
      sb.append(mvnSettingsFile);
4✔
323
    }
324
    if (securityFileExists) {
2✔
325
      if (!sb.isEmpty()) {
3✔
326
        sb.append(" ");
4✔
327
      }
328
      sb.append(getSettingsSecurityProperty());
5✔
329
    }
330
    return sb.toString();
3✔
331
  }
332

333
  private String getSettingsSecurityProperty() {
334
    return "-Dsettings.security=" + this.getMavenConfigurationFolder().resolve(SETTINGS_SECURITY_FILE).toString().replace("\\", "\\\\");
10✔
335
  }
336

337
  /**
338
   * @return the {@link Path} to the local maven repository.
339
   */
340
  public Path getLocalRepository() {
341
    return IdeVariables.M2_REPO.get(this.context);
×
342
  }
343

344
  /**
345
   * @param artifact the {@link MvnArtifact}.
346
   */
347
  public void downloadArtifact(MvnArtifact artifact) {
348

349
    this.context.newStep("Download artifact " + artifact).run(() -> {
×
350
      runTool(List.of("dependency:get", "-Dartifact=" + artifact.getKey()));
×
351
    });
×
352
  }
×
353

354
  /**
355
   * @param artifact the {@link MvnArtifact}.
356
   * @return the {@link Path} to the {@link MvnArtifact} that was downloaded if not already present.
357
   */
358
  public Path getOrDownloadArtifact(MvnArtifact artifact) {
359

360
    Path artifactPath = getLocalRepository().resolve(artifact.getPath());
×
361
    if (!Files.exists(artifactPath)) {
×
362
      downloadArtifact(artifact);
×
363
      assert (Files.exists(artifactPath));
×
364
    }
365
    return artifactPath;
×
366
  }
367

368
  @Override
369
  public Path findBuildDescriptor(Path directory) {
370

371
    Path buildDescriptor = directory.resolve(POM_XML);
4✔
372
    if (Files.exists(buildDescriptor)) {
5✔
373
      return buildDescriptor;
2✔
374
    }
375
    return super.findBuildDescriptor(directory);
4✔
376
  }
377
}
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