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

devonfw / IDEasy / 29803149984

21 Jul 2026 05:08AM UTC coverage: 72.439% (-0.04%) from 72.476%
29803149984

Pull #2179

github

web-flow
Merge c6c1d1813 into 3221b7580
Pull Request #2179: Feature/2165 interactive template variables

4961 of 7574 branches covered (65.5%)

Branch coverage included in aggregate %.

12804 of 16950 relevant lines covered (75.54%)

3.2 hits per line

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

81.65
cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java
1
package com.devonfw.tools.ide.environment;
2

3
import java.nio.file.Path;
4
import java.util.ArrayList;
5
import java.util.HashMap;
6
import java.util.List;
7
import java.util.Map;
8
import java.util.regex.Matcher;
9

10
import org.slf4j.Logger;
11
import org.slf4j.LoggerFactory;
12
import org.slf4j.event.Level;
13

14
import com.devonfw.tools.ide.context.IdeContext;
15
import com.devonfw.tools.ide.variable.IdeVariables;
16
import com.devonfw.tools.ide.variable.VariableDefinition;
17
import com.devonfw.tools.ide.variable.VariableSyntax;
18
import com.devonfw.tools.ide.version.VersionIdentifier;
19

20
/**
21
 * Abstract base implementation of {@link EnvironmentVariables}.
22
 */
23
public abstract class AbstractEnvironmentVariables implements EnvironmentVariables {
24

25
  private static final Logger LOG = LoggerFactory.getLogger(AbstractEnvironmentVariables.class);
4✔
26

27
  /**
28
   * When we replace variable expressions with their value the resulting {@link String} can change in size (shrink or grow). By adding a bit of extra capacity
29
   * we reduce the chance that the capacity is too small and a new buffer array has to be allocated and array-copy has to be performed.
30
   */
31
  private static final int EXTRA_CAPACITY = 8;
32

33
  private static final String SELF_REFERENCING_NOT_FOUND = "";
34

35
  private static final int MAX_RECURSION = 9;
36

37
  /**
38
   * @see #getParent()
39
   */
40
  protected final AbstractEnvironmentVariables parent;
41

42
  /**
43
   * The {@link IdeContext} instance.
44
   */
45
  protected final IdeContext context;
46

47
  private VariableSource source;
48

49
  /**
50
   * The constructor.
51
   *
52
   * @param parent the parent {@link EnvironmentVariables} to inherit from.
53
   * @param context the {@link IdeContext}.
54
   */
55
  public AbstractEnvironmentVariables(AbstractEnvironmentVariables parent, IdeContext context) {
56

57
    super();
2✔
58
    this.parent = parent;
3✔
59
    if (context == null) {
2!
60
      if (parent == null) {
×
61
        throw new IllegalArgumentException("parent and logger must not both be null!");
×
62
      }
63
      this.context = parent.context;
×
64
    } else {
65
      this.context = context;
3✔
66
    }
67
  }
1✔
68

69
  @Override
70
  public EnvironmentVariables getParent() {
71

72
    return this.parent;
3✔
73
  }
74

75
  @Override
76
  public Path getPropertiesFilePath() {
77

78
    return null;
2✔
79
  }
80

81
  @Override
82
  public Path getLegacyPropertiesFilePath() {
83

84
    return null;
2✔
85
  }
86

87
  @Override
88
  public VariableSource getSource() {
89

90
    if (this.source == null) {
3✔
91
      this.source = new VariableSource(getType(), getPropertiesFilePath());
9✔
92
    }
93
    return this.source;
3✔
94
  }
95

96
  /**
97
   * @param name the name of the variable to check.
98
   * @return {@code true} if the variable shall be exported, {@code false} otherwise.
99
   */
100
  protected boolean isExported(String name) {
101

102
    if (this.parent != null) {
3✔
103
      return this.parent.isExported(name);
5✔
104
    }
105
    return false;
2✔
106
  }
107

108
  @Override
109
  public final List<VariableLine> collectVariables() {
110

111
    return collectVariables(false);
4✔
112
  }
113

114
  @Override
115
  public final List<VariableLine> collectExportedVariables() {
116

117
    return collectVariables(true);
4✔
118
  }
119

120
  private final List<VariableLine> collectVariables(boolean onlyExported) {
121

122
    Map<String, VariableLine> variables = new HashMap<>();
4✔
123
    collectVariables(variables, onlyExported, this);
5✔
124
    return new ArrayList<>(variables.values());
6✔
125
  }
126

127
  /**
128
   * @param variables the {@link Map} where to add the names of the variables defined here as keys, and their corresponding source as value.
129
   */
130
  protected void collectVariables(Map<String, VariableLine> variables, boolean onlyExported, AbstractEnvironmentVariables resolver) {
131

132
    if (this.parent != null) {
3✔
133
      this.parent.collectVariables(variables, onlyExported, resolver);
6✔
134
    }
135
  }
1✔
136

137
  protected VariableLine createVariableLine(String name, boolean onlyExported, AbstractEnvironmentVariables resolver) {
138

139
    boolean export = resolver.isExported(name);
4✔
140
    if (!onlyExported || export) {
4✔
141
      String value = resolver.get(name, false);
5✔
142
      if (value != null) {
2✔
143
        return VariableLine.of(export, name, value, getSource());
7✔
144
      }
145
    }
146
    return null;
2✔
147
  }
148

149
  /**
150
   * @param propertiesFolderPath the {@link Path} to the folder containing the {@link #getPropertiesFilePath() properties file} of the child
151
   *     {@link EnvironmentVariables}.
152
   * @param type the {@link #getType() type}.
153
   * @return the new {@link EnvironmentVariables}.
154
   */
155
  public AbstractEnvironmentVariables extend(Path propertiesFolderPath, EnvironmentVariablesType type) {
156

157
    return new EnvironmentVariablesPropertiesFile(this, type, propertiesFolderPath, null, this.context);
10✔
158
  }
159

160
  /**
161
   * @return a new child {@link EnvironmentVariables} that will resolve variables recursively or this instance itself if already satisfied.
162
   */
163
  public EnvironmentVariables resolved() {
164

165
    return new EnvironmentVariablesResolved(this);
5✔
166
  }
167

168
  @Override
169
  public String resolve(String string, Object source) {
170
    return resolveRecursive(string, source, 0, this, new ResolveContext(source, string, false, VariableSyntax.CURLY));
14✔
171
  }
172

173
  @Override
174
  public String resolve(String string, Object source, boolean legacySupport) {
175

176
    return resolveRecursive(string, source, 0, this, new ResolveContext(source, string, legacySupport, null));
14✔
177
  }
178

179
  /**
180
   * This method is called recursively. This allows you to resolve variables that are defined by other variables.
181
   *
182
   * @param value the {@link String} that potentially contains variables in the syntax "${«variable«}". Those will be resolved by this method and replaced
183
   *     with their {@link #get(String) value}.
184
   * @param source the source where the {@link String} to resolve originates from. Should have a reasonable {@link Object#toString() string representation}
185
   *     that will be used in error or log messages if a variable could not be resolved.
186
   * @param recursion the current recursion level. This is used to interrupt endless recursion.
187
   * @param resolvedVars this is a reference to an object of {@link EnvironmentVariablesResolved} being the lowest level in the
188
   *     {@link EnvironmentVariablesType hierarchy} of variables. In case of a self-referencing variable {@code x} the resolving has to continue one level
189
   *     higher in the {@link EnvironmentVariablesType hierarchy} to avoid endless recursion. The {@link EnvironmentVariablesResolved} is then used if another
190
   *     variable {@code y} must be resolved, since resolving this variable has to again start at the lowest level. For example: For levels {@code l1, l2} with
191
   *     {@code l1 < l2} and {@code x=${x} foo} and {@code y=bar} defined at level {@code l1} and {@code x=test ${y}} defined at level {@code l2}, {@code x} is
192
   *     first resolved at level {@code l1} and then up the {@link EnvironmentVariablesType hierarchy} at {@code l2} to avoid endless recursion. However,
193
   *     {@code y} must be resolved starting from the lowest level in the {@link EnvironmentVariablesType hierarchy} and therefore
194
   *     {@link EnvironmentVariablesResolved} is used.
195
   * @param context the {@link ResolveContext}.
196
   * @return the given {@link String} with the variables resolved.
197
   */
198
  private String resolveRecursive(String value, Object source, int recursion, AbstractEnvironmentVariables resolvedVars, ResolveContext context) {
199

200
    if (value == null) {
2!
201
      return null;
×
202
    }
203
    if (recursion > MAX_RECURSION) {
3!
204
      throw new IllegalStateException(
×
205
          "Reached maximum recursion resolving " + value + " for root variable " + context.rootSrc + " with value '" + context.rootValue + "'.");
206
    }
207
    recursion++;
1✔
208

209
    String resolved;
210
    if (context.syntax == null) {
3✔
211
      resolved = resolveWithSyntax(value, source, recursion, resolvedVars, context, VariableSyntax.SQUARE);
9✔
212
      if (context.legacySupport) {
3✔
213
        resolved = resolveWithSyntax(resolved, source, recursion, resolvedVars, context, VariableSyntax.CURLY);
10✔
214
      }
215
    } else {
216
      resolved = resolveWithSyntax(value, source, recursion, resolvedVars, context, context.syntax);
10✔
217
    }
218
    return resolved;
2✔
219
  }
220

221
  private String resolveWithSyntax(final String value, final Object src, final int recursion, final AbstractEnvironmentVariables resolvedVars,
222
      final ResolveContext context, final VariableSyntax syntax) {
223

224
    Matcher matcher = syntax.getPattern().matcher(value);
5✔
225
    if (!matcher.find()) {
3✔
226
      return value;
2✔
227
    }
228
    StringBuilder sb = new StringBuilder(value.length() + EXTRA_CAPACITY);
8✔
229
    do {
230
      String variableName = syntax.getVariable(matcher);
4✔
231
      if (variableName == null) {
2✔
232
        // current match is not a plain variable but an ask expression like $[ask:MY_VARIABLE] or $[secret:MY_TOKEN]
233
        String askVariable = syntax.getAskVariable(matcher);
4✔
234
        if (resolvedVars.getValue(askVariable, false) == null) {
5✔
235
          // variable is undefined so we have to ask the user interactively
236
          String replacement = resolveAskExpression(askVariable, syntax.isSecret(matcher), resolvedVars, src);
9✔
237
          if (replacement == null) {
2!
238
            continue; // could not be resolved - leave expression untouched (warning has already been logged)
×
239
          }
240
          matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
6✔
241
          continue;
1✔
242
        }
243
        // variable is already defined - fall through and resolve it like a plain variable
244
        variableName = askVariable;
2✔
245
      }
246
      String variableValue = resolvedVars.getValue(variableName, false);
5✔
247
      if (variableValue == null) {
2✔
248
        Level logLevel = Level.WARN;
2✔
249
        if (context.legacySupport && (syntax == VariableSyntax.CURLY)) {
3!
250
          logLevel = Level.INFO;
×
251
        }
252
        String var = matcher.group();
3✔
253
        if (recursion > 1) {
3✔
254
          LOG.atLevel(logLevel).log("Undefined variable {} in '{}' at '{}={}'", var, context.rootSrc, src, value);
25✔
255
        } else {
256
          LOG.atLevel(logLevel).log("Undefined variable {} in '{}'", var, src);
7✔
257
        }
258
        continue;
1✔
259
      }
260
      EnvironmentVariables lowestFound = findVariable(variableName);
4✔
261
      if ((lowestFound == null) || !lowestFound.getFlat(variableName).equals(value)) {
8✔
262
        // looking for "variableName" starting from resolved upwards the hierarchy
263
        String replacement = resolvedVars.resolveRecursive(variableValue, variableName, recursion, resolvedVars, context);
8✔
264
        matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
6✔
265
      } else { // is self referencing
1✔
266
        // finding next occurrence of "variableName" up the hierarchy of EnvironmentVariablesType
267
        EnvironmentVariables next = lowestFound.getParent();
3✔
268
        while (next != null) {
2✔
269
          if (next.getFlat(variableName) != null) {
4✔
270
            break;
1✔
271
          }
272
          next = next.getParent();
4✔
273
        }
274
        if (next == null) {
2✔
275
          matcher.appendReplacement(sb, Matcher.quoteReplacement(SELF_REFERENCING_NOT_FOUND));
6✔
276
          continue;
1✔
277
        }
278
        // resolving a self referencing variable one level up the hierarchy of EnvironmentVariablesType, i.e. at "next",
279
        // to avoid endless recursion
280
        String replacement = ((AbstractEnvironmentVariables) next).resolveRecursive(next.getFlat(variableName), variableName, recursion, resolvedVars, context
11✔
281
        );
282
        matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
6✔
283

284
      }
285
    } while (matcher.find());
3✔
286
    matcher.appendTail(sb);
4✔
287

288
    return sb.toString();
3✔
289
  }
290

291
  /**
292
   * Resolves an interactive ask expression like {@code $[ask:MY_VARIABLE]} or {@code $[secret:MY_TOKEN]} by prompting the user. The entered value is persisted
293
   * in his local {@link EnvironmentVariablesType#CONF configuration} so he will not be asked again. The caller has to ensure that the variable is undefined.
294
   *
295
   * @param variableName the name of the variable to resolve.
296
   * @param secret {@code true} if the input shall be masked while typing.
297
   * @param resolvedVars the {@link EnvironmentVariablesResolved} used to persist the entered value.
298
   * @param src the source of the expression (used in the prompt).
299
   * @return the entered value or {@code null} if it could not be determined (the expression is then left untouched).
300
   */
301
  private String resolveAskExpression(String variableName, boolean secret, AbstractEnvironmentVariables resolvedVars, Object src) {
302

303
    if ((this.context == null) || this.context.isBatchMode()) {
7!
304
      LOG.warn("Undefined variable {} in '{}' cannot be resolved interactively in batch mode.", variableName, src);
×
305
      return null;
×
306
    }
307
    String message = "Please enter " + (secret ? "secret " : "") + "value for variable " + variableName + " (required by " + src + "):";
10✔
308
    String input;
309
    if (secret) {
2✔
310
      input = this.context.askForSecret(message);
6✔
311
    } else {
312
      input = this.context.askForInput(message);
5✔
313
    }
314
    EnvironmentVariables confVariables = resolvedVars.getByType(EnvironmentVariablesType.CONF);
4✔
315
    if (confVariables instanceof EnvironmentVariablesPropertiesFile propertiesFile) {
6!
316
      propertiesFile.set(variableName, input, false);
6✔
317
      propertiesFile.save();
2✔
318
      LOG.info("Variable {} has been saved to {}", variableName, propertiesFile.getPropertiesFilePath());
7✔
319
    } else {
320
      LOG.warn("Could not persist variable {} - you may be asked for it again.", variableName);
×
321
    }
322
    return input;
2✔
323
  }
324

325
  /**
326
   * Like {@link #get(String)} but with higher-level features including to resolve {@link IdeVariables} with their default values.
327
   *
328
   * @param name the name of the variable to get.
329
   * @param ignoreDefaultValue - {@code true} if the {@link VariableDefinition#getDefaultValue(IdeContext) default value} of a potential
330
   *     {@link VariableDefinition} shall be ignored, {@code false} to return default instead of {@code null}.
331
   * @return the value of the variable.
332
   */
333
  protected String getValue(String name, boolean ignoreDefaultValue) {
334

335
    VariableDefinition<?> var = IdeVariables.get(name);
3✔
336
    String value;
337
    if ((var != null) && var.isForceDefaultValue()) {
5✔
338
      value = var.getDefaultValueAsString(this.context);
6✔
339
    } else {
340
      value = this.parent.get(name, false);
6✔
341
    }
342
    if ((value == null) && (var != null)) {
4✔
343
      String key = var.getName();
3✔
344
      if (!name.equals(key)) {
4✔
345
        // try new name (e.g. IDE_TOOLS or IDE_HOME) if no value could be found by given legacy name (e.g.
346
        // DEVON_IDE_TOOLS or DEVON_IDE_HOME)
347
        value = this.parent.get(key, false);
6✔
348
      }
349
      if ((value == null) && !ignoreDefaultValue) {
4!
350
        value = var.getDefaultValueAsString(this.context);
5✔
351
      }
352
    } else if ((value != null) && (var != null) && var.isDefaultValueAppended()) {
8✔
353
      // if user has set a value, append IDEasy's default to it instead of replacing it
354
      value = mergeWithDefault(value, var.getDefaultValueAsString(this.context));
7✔
355
    }
356
    if ((value != null) && (value.startsWith("~/"))) {
6✔
357
      value = this.context.getUserHome() + value.substring(1);
9✔
358
    }
359
    return value;
2✔
360
  }
361

362
  @Override
363
  public String inverseResolve(String string, Object src, VariableSyntax syntax) {
364

365
    String result = string;
×
366
    for (VariableDefinition<?> variable : IdeVariables.VARIABLES) {
×
367
      if (variable != IdeVariables.PATH) {
×
368
        String name = variable.getName();
×
369
        String value = get(name);
×
370
        if (value == null) {
×
371
          value = variable.getDefaultValueAsString(this.context);
×
372
        }
373
        if (value != null) {
×
374
          result = result.replace(value, syntax.create(name));
×
375
        }
376
      }
377
    }
×
378
    if (!result.equals(string)) {
×
379
      LOG.trace("Inverse resolved '{}' to '{}' from {}.", string, result, src);
×
380
    }
381
    return result;
×
382
  }
383

384
  @Override
385
  public VersionIdentifier getToolVersion(String tool) {
386

387
    String variable = EnvironmentVariables.getToolVersionVariable(tool);
3✔
388
    String value = get(variable);
4✔
389
    if (value == null) {
2✔
390
      return VersionIdentifier.LATEST;
2✔
391
    } else if (value.isEmpty()) {
3✔
392
      LOG.warn("Variable {} is configured with empty value, please fix your configuration.", variable);
4✔
393
      return VersionIdentifier.LATEST;
2✔
394
    }
395
    VersionIdentifier version = VersionIdentifier.of(value);
3✔
396
    if (version == null) {
2!
397
      // can actually never happen, but for robustness
398
      version = VersionIdentifier.LATEST;
×
399
    }
400
    return version;
2✔
401
  }
402

403
  @Override
404
  public String toString() {
405

406
    return getSource().toString();
×
407
  }
408

409
  /**
410
   * Simple record for the immutable arguments of recursive resolve methods.
411
   *
412
   * @param rootSrc the root source where the {@link String} to resolve originates from.
413
   * @param rootValue the root value to resolve.
414
   * @param legacySupport flag for legacy support (see {@link #resolve(String, Object, boolean)}). Only considered if {@link #syntax()} is {@code null}.
415
   * @param syntax the explicit {@link VariableSyntax} to use.
416
   */
417
  private static record ResolveContext(Object rootSrc, String rootValue, boolean legacySupport, VariableSyntax syntax) {
15✔
418

419
  }
420

421
  /**
422
   * Appends IDEasy's default to the user's input value, stripping {@code -s <path>} and {@code -Dsettings.security=<path>}.
423
   *
424
   * @param value the user-defined value.
425
   * @param defaultValue IDEasy's default value.
426
   * @return the merged value.
427
   */
428
  static String mergeWithDefault(String value, String defaultValue) {
429
    if (defaultValue == null || defaultValue.isEmpty()) {
5✔
430
      return value;
2✔
431
    }
432
    StringBuilder merged = new StringBuilder();
4✔
433
    String[] tokens = value.trim().split("\\s+");
5✔
434
    for (int i = 0; i < tokens.length; i++) {
8✔
435
      String token = tokens[i];
4✔
436
      if (token.equals("-s")) {
4✔
437
        i++; // skip the path argument that follows
2✔
438
      } else if (!token.startsWith("-Dsettings.security=")) {
4✔
439
        merged.append(token).append(' ');
6✔
440
      }
441
    }
442
    return merged.append(defaultValue).toString();
5✔
443
  }
444

445
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc