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

devonfw / IDEasy / 25505184832

07 May 2026 03:23PM UTC coverage: 70.917% (+0.2%) from 70.741%
25505184832

Pull #1858

github

web-flow
Merge f96fc2947 into fd215c395
Pull Request #1858: #1457: Improve CLI error messages with suggestions

4523 of 7036 branches covered (64.28%)

Branch coverage included in aggregate %.

11527 of 15596 relevant lines covered (73.91%)

3.13 hits per line

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

72.59
cli/src/main/java/com/devonfw/tools/ide/property/Property.java
1
package com.devonfw.tools.ide.property;
2

3
import java.util.ArrayList;
4
import java.util.List;
5
import java.util.Objects;
6
import java.util.function.Consumer;
7

8
import org.slf4j.Logger;
9
import org.slf4j.LoggerFactory;
10

11
import com.devonfw.tools.ide.cli.CliArgument;
12
import com.devonfw.tools.ide.cli.CliArguments;
13
import com.devonfw.tools.ide.commandlet.Commandlet;
14
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
15
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorAdapter;
16
import com.devonfw.tools.ide.context.IdeContext;
17
import com.devonfw.tools.ide.validation.PropertyValidator;
18
import com.devonfw.tools.ide.validation.ValidationResult;
19
import com.devonfw.tools.ide.validation.ValidationState;
20

21
/**
22
 * A {@link Property} is a simple container for a {@link #getValue() value} with a fixed {@link #getName() name} and {@link #getValueType() type}. Further we
23
 * use a {@link Property} as {@link CliArgument CLI argument} so it is either an {@link #isOption() option} or a {@link #isValue() value}.<br> In classic Java
24
 * Beans a property only exists implicit as a combination of a getter and a setter. This class makes it an explicit construct that allows to
25
 * {@link #getValue() get} and {@link #setValue(Object) set} the value of a property easily in a generic way including to retrieve its {@link #getName() name}
26
 * and {@link #getValueType() type}. Besides simplification this also prevents the use of reflection for generic CLI parsing with assigning and validating
27
 * arguments what is beneficial for compiling the Java code to a native image using GraalVM.
28
 *
29
 * @param <V> the {@link #getValueType() value type}.
30
 */
31
public abstract class Property<V> {
32

33
  private static final Logger LOG = LoggerFactory.getLogger(Property.class);
4✔
34

35
  private static final String INVALID_ARGUMENT = "Invalid CLI argument '{}' for property '{}' of commandlet '{}'";
36

37
  private static final String INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE = INVALID_ARGUMENT + ": {}";
38

39
  /** @see #getName() */
40
  protected final String name;
41

42
  /** @see #getAlias() */
43
  protected final String alias;
44

45
  /** @see #isRequired() */
46
  protected final boolean required;
47

48
  private final PropertyValidator<V> validator;
49

50
  /** @see #isMultiValued() */
51
  private final boolean multivalued;
52

53
  /** @see #getValue() */
54
  protected final List<V> value = new ArrayList<>();
10✔
55

56
  /** The last invalid value that was attempted to be parsed. */
57
  private String lastInvalidValue;
58

59
  /**
60
   * The constructor.
61
   *
62
   * @param name the {@link #getName() property name}.
63
   * @param required the {@link #isRequired() required flag}.
64
   * @param alias the {@link #getAlias() property alias}.
65
   */
66
  public Property(String name, boolean required, String alias) {
67

68
    super();
2✔
69
    this.name = name;
3✔
70
    this.required = required;
3✔
71
    this.alias = alias;
3✔
72
    this.multivalued = false;
3✔
73
    this.validator = null;
3✔
74
  }
1✔
75

76
  /**
77
   * The constructor.
78
   *
79
   * @param name the {@link #getName() property name}.
80
   * @param required the {@link #isRequired() required flag}.
81
   * @param alias the {@link #getAlias() property alias}.
82
   * @param multivalued the boolean flag about multiple arguments
83
   * @param validator the {@link Consumer} used to {@link #validate() validate} the {@link #getValue() value}.
84
   */
85
  public Property(String name, boolean required, String alias, boolean multivalued, PropertyValidator<V> validator) {
86

87
    super();
2✔
88
    this.name = name;
3✔
89
    this.required = required;
3✔
90
    this.alias = alias;
3✔
91
    this.validator = validator;
3✔
92
    this.multivalued = multivalued;
3✔
93
  }
1✔
94

95
  /**
96
   * @return the name of this property. Will be the empty {@link String} for a {@link #isValue() value} property that is not a keyword.
97
   */
98
  public String getName() {
99

100
    return this.name;
3✔
101
  }
102

103
  /**
104
   * @return the alias of this property or {@code null} for none.
105
   * @see #isOption()
106
   */
107
  public String getAlias() {
108

109
    return this.alias;
3✔
110
  }
111

112
  /**
113
   * @return the {@link #getName() name} or the {@link #getAlias() alias} if {@link #getName() name} is {@link String#isEmpty() empty}.
114
   */
115
  public String getNameOrAlias() {
116

117
    if (this.name.isEmpty()) {
4✔
118
      return this.alias;
3✔
119
    }
120
    return this.name;
3✔
121
  }
122

123
  /**
124
   * @return {@code true} if this property is required (if argument is not present the {@link Commandlet} cannot be invoked), {@code false} otherwise (if
125
   *     optional).
126
   */
127
  public boolean isRequired() {
128

129
    return this.required;
3✔
130
  }
131

132
  /**
133
   * @return {@code true} if a value is expected as additional CLI argument.
134
   */
135
  public boolean isExpectValue() {
136

137
    return "".equals(this.name);
×
138
  }
139

140
  /**
141
   * Determines if this {@link Property} is an option. Canonical options have a long-option {@link #getName() name} (e.g. "--force") and a short-option
142
   * {@link #getAlias() alias} (e.g. "-f").
143
   *
144
   * @return {@code true} if this {@link Property} is an option, {@code false} otherwise (if a positional argument).
145
   */
146
  public boolean isOption() {
147

148
    return this.name.startsWith("-");
5✔
149
  }
150

151
  /**
152
   * @return {@code true} if this {@link Property} forces an implicit {@link CliArgument#isEndOptions() end-options} as if "--" was provided before its first
153
   *     {@link CliArgument argument}.
154
   */
155
  public boolean isEndOptions() {
156

157
    return isMultiValued();
×
158
  }
159

160
  /**
161
   * Determines if this {@link Property} is multi-valued and accepts any number of values. A multi-valued {@link Property} needs to be the last {@link Property}
162
   * of a {@link Commandlet}.
163
   *
164
   * @return {@code true} if multi-valued, {@code false} otherwise.
165
   */
166
  public boolean isMultiValued() {
167

168
    return this.multivalued;
3✔
169
  }
170

171
  /**
172
   * Determines if this a value {@link Property}. Such value is either a {@link KeywordProperty} with the keyword as {@link #getName() name} or a raw indexed
173
   * value argument. In the latter case the command-line argument at this index will be the immediate value of the {@link Property}, the {@link #getName() name}
174
   * is {@link String#isEmpty() empty} and the {@link #getAlias() alias} is a logical name of the value to display to users.
175
   *
176
   * @return {@code true} if value, {@code false} otherwise.
177
   */
178
  public boolean isValue() {
179

180
    return !isOption();
7✔
181
  }
182

183
  /**
184
   * @return the {@link Class} reflecting the type of the {@link #getValue() value}.
185
   */
186
  public abstract Class<V> getValueType();
187

188
  /**
189
   * @return the value of this property.
190
   * @see #setValue(Object)
191
   */
192
  public V getValue() {
193

194
    if (this.value.isEmpty()) {
4✔
195
      return null;
2✔
196
    } else {
197
      return this.value.getFirst();
4✔
198
    }
199
  }
200

201
  /**
202
   * @param i the position to get.
203
   * @return the value of this property.
204
   */
205
  public V getValue(int i) {
206

207
    return this.value.get(i);
5✔
208
  }
209

210
  /**
211
   * @return amount of values.
212
   */
213
  public int getValueCount() {
214

215
    return this.value.size();
4✔
216
  }
217

218
  /**
219
   * @return the {@link #getValue() value} as {@link String}.
220
   * @see #setValueAsString(String, IdeContext)
221
   */
222
  public String getValueAsString() {
223

224
    if (getValue() == null) {
3!
225
      return null;
×
226
    }
227
    return format(getValue());
5✔
228
  }
229

230
  /**
231
   * @return a {@link List} containing all {@link #getValue(int) values}. This method only makes sense for {@link #isMultiValued() multi valued} properties.
232
   */
233
  public List<V> asList() {
234

235
    return new ArrayList<>(this.value);
6✔
236
  }
237

238
  /**
239
   * @param valueToFormat the value to format.
240
   * @return the given {@code value} formatted as {@link String}.
241
   */
242
  protected String format(V valueToFormat) {
243

244
    return valueToFormat.toString();
3✔
245
  }
246

247
  /**
248
   * @param value the new {@link #getValue() value} to set.
249
   * @see #getValue()
250
   */
251
  public void setValue(V value) {
252

253
    if (!this.multivalued) {
3✔
254
      this.value.clear();
3✔
255
    }
256
    this.value.add(value);
5✔
257
  }
1✔
258

259
  /**
260
   * Clears the {@link #value value} list.
261
   */
262
  public void clearValue() {
263

264
    this.value.clear();
3✔
265
  }
1✔
266

267
  /**
268
   * @param value the value to add to the {@link List} of values.
269
   * @see #isMultiValued()
270
   */
271
  public void addValue(V value) {
272

273
    if (!this.multivalued) {
3!
274
      throw new IllegalStateException("not multivalued");
×
275
    }
276
    this.value.add(value);
5✔
277
  }
1✔
278

279
  /**
280
   * @param value the new {@link #getValue() value} to set.
281
   * @param i the position to set.
282
   */
283
  public void setValue(V value, int i) {
284

285
    this.value.set(i, value);
6✔
286
  }
1✔
287

288
  /**
289
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
290
   * @param context the {@link IdeContext}
291
   * @see #getValueAsString()
292
   */
293
  public void setValueAsString(String valueAsString, IdeContext context) {
294

295
    if (valueAsString == null) {
2!
296
      setValue(getNullValue());
×
297
    } else {
298
      setValue(parse(valueAsString, context));
6✔
299
    }
300
  }
1✔
301

302
  /**
303
   * Like {@link #setValueAsString(String, IdeContext)} but with exception handling.
304
   *
305
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
306
   * @param context the {@link IdeContext}
307
   * @param commandlet the {@link Commandlet} owning this property.
308
   * @return {@code true} if the value has been assigned successfully, {@code false} otherwise (an error occurred).
309
   */
310
  public final boolean assignValueAsString(String valueAsString, IdeContext context, Commandlet commandlet) {
311

312
    try {
313
      setValueAsString(valueAsString, context);
4✔
314
      this.lastInvalidValue = null;
3✔
315
      return true;
2✔
316
    } catch (Exception e) {
1✔
317
      this.lastInvalidValue = valueAsString;
3✔
318
      if (e instanceof IllegalArgumentException) {
3!
319
        LOG.warn(INVALID_ARGUMENT, valueAsString, getNameOrAlias(), commandlet.getName());
20✔
320
      } else {
321
        LOG.warn(INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE, valueAsString, getNameOrAlias(), commandlet.getName(), e.getMessage());
×
322
      }
323
      return false;
2✔
324
    }
325
  }
326

327
  /**
328
   * @return the last invalid value that was attempted to be parsed, or {@code null} if no invalid value was attempted or the last attempt was successful.
329
   */
330
  public String getLastInvalidValue() {
331
    return this.lastInvalidValue;
3✔
332
  }
333

334
  /**
335
   * Clears the last invalid value.
336
   */
337
  public void clearLastInvalidValue() {
338
    this.lastInvalidValue = null;
3✔
339
  }
1✔
340

341
  /**
342
   * @return the {@code null} value.
343
   */
344
  protected V getNullValue() {
345

346
    return null;
×
347
  }
348

349
  /**
350
   * @param valueAsString the value to parse given as {@link String}.
351
   * @param context the {@link IdeContext}.
352
   * @return the parsed value.
353
   */
354
  public abstract V parse(String valueAsString, IdeContext context);
355

356
  /**
357
   * @param args the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
358
   * @param context the {@link IdeContext}.
359
   * @param commandlet the {@link Commandlet} owning this property.
360
   * @param collector the {@link CompletionCandidateCollector}.
361
   * @return {@code true} if it matches, {@code false} otherwise.
362
   */
363
  public boolean apply(CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
364

365
    return apply(this.name, args, context, commandlet, collector);
9✔
366
  }
367

368
  /**
369
   * @param normalizedName the {@link #getName() name} or potentially a normalized form of it (see {@link KeywordProperty}).
370
   * @param args the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
371
   * @param context the {@link IdeContext}.
372
   * @param commandlet the {@link Commandlet} owning this property.
373
   * @param collector the {@link CompletionCandidateCollector}.
374
   * @return {@code true} if it matches, {@code false} otherwise.
375
   */
376
  protected boolean apply(String normalizedName, CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
377

378
    CliArgument argument = args.current();
3✔
379
    if (argument.isCompletion()) {
3✔
380
      int size = collector.getCandidates().size();
4✔
381
      complete(normalizedName, argument, args, context, commandlet, collector);
8✔
382
      return (collector.getCandidates().size() > size);
9✔
383
    }
384
    boolean option = normalizedName.startsWith("-");
4✔
385
    if (option && !argument.isOption()) {
5✔
386
      return false;
2✔
387
    }
388
    if (!option && argument.isOption() && (argument.get().length() > 1) && args.isSplitShortOpts()) {
13✔
389
      return false;
2✔
390
    }
391
    String argValue = null;
2✔
392
    boolean lookahead = false;
2✔
393
    if (normalizedName.isEmpty()) {
3✔
394
      argValue = argument.get();
4✔
395
    } else {
396
      if (!matches(argument.getKey())) {
5✔
397
        return false;
2✔
398
      }
399
      argValue = argument.getValue();
3✔
400
      if (argValue == null) {
2!
401
        argument = args.next();
3✔
402
        if (argument.isCompletion()) {
3✔
403
          completeValue(argument.get(), context, commandlet, collector);
7✔
404
          return true;
2✔
405
        } else {
406
          if (!argument.isEnd()) {
3✔
407
            argValue = argument.get();
3✔
408
          }
409
          lookahead = true;
2✔
410
        }
411
      }
412
    }
413
    return applyValue(argValue, lookahead, args, context, commandlet, collector);
9✔
414
  }
415

416
  /**
417
   * @param argValue the value to set as {@link String}.
418
   * @param lookahead - {@code true} if the given {@code argValue} is taken as lookahead from the next value, {@code false} otherwise.
419
   * @param args the {@link CliArguments}.
420
   * @param context the {@link IdeContext}.
421
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
422
   * @param collector the {@link CompletionCandidateCollector}.
423
   * @return {@code true} if it matches, {@code false} otherwise.
424
   */
425
  protected boolean applyValue(String argValue, boolean lookahead, CliArguments args, IdeContext context, Commandlet commandlet,
426
      CompletionCandidateCollector collector) {
427

428
    boolean success = assignValueAsString(argValue, context, commandlet);
6✔
429

430
    if (success) {
2✔
431
      if (this.multivalued) {
3✔
432
        while (success && args.hasNext()) {
5!
433
          CliArgument arg = args.next();
×
434
          success = assignValueAsString(arg.get(), context, commandlet);
×
435
        }
×
436
      }
437
    }
438
    args.next();
3✔
439
    return success;
2✔
440
  }
441

442
  /**
443
   * Performs auto-completion for the {@code arg}.
444
   *
445
   * @param normalizedName the {@link #getName() name} or potentially a normalized form of it (see {@link KeywordProperty}).
446
   * @param argument the {@link CliArgument CLI argument}.
447
   * @param args the {@link CliArguments}.
448
   * @param context the {@link IdeContext}.
449
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
450
   * @param collector the {@link CompletionCandidateCollector}.
451
   */
452
  protected void complete(String normalizedName, CliArgument argument, CliArguments args, IdeContext context, Commandlet commandlet,
453
      CompletionCandidateCollector collector) {
454

455
    String arg = argument.get();
3✔
456
    if (normalizedName.isEmpty()) {
3✔
457
      int count = collector.getCandidates().size();
4✔
458
      completeValue(arg, context, commandlet, collector);
6✔
459
      if (collector.getCandidates().size() > count) {
5✔
460
        args.next();
3✔
461
      }
462
      return;
1✔
463
    }
464
    if (normalizedName.startsWith(arg)) {
4✔
465
      collector.add(normalizedName, null, this, commandlet);
6✔
466
    }
467
    if (this.alias != null) {
3✔
468
      if (this.alias.startsWith(arg)) {
5✔
469
        collector.add(this.alias, null, this, commandlet);
8✔
470
      } else if ((this.alias.length() == 2) && (this.alias.charAt(0) == '-') && argument.isShortOption()) {
14!
471
        char opt = this.alias.charAt(1); // e.g. arg="-do" and alias="-f" -complete-> "-dof"
5✔
472
        if (arg.indexOf(opt) < 0) {
4✔
473
          collector.add(arg + opt, null, this, commandlet);
8✔
474
        }
475
      }
476
    }
477
    String value = argument.getValue();
3✔
478
    if (value != null) {
2!
479
      String key = argument.getKey();
×
480
      if (normalizedName.equals(key) || Objects.equals(this.alias, key)) {
×
481
        completeValue(value, context, commandlet, new CompletionCandidateCollectorAdapter(key + "=", collector));
×
482
      }
483
    }
484
  }
1✔
485

486
  /**
487
   * Performs auto-completion for the {@code arg} as {@link #getValue() property value}.
488
   *
489
   * @param arg the {@link CliArgument#get() CLI argument}.
490
   * @param context the {@link IdeContext}.
491
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
492
   * @param collector the {@link CompletionCandidateCollector}.
493
   */
494
  protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
495

496
  }
1✔
497

498
  /**
499
   * @param nameOrAlias the potential {@link #getName() name} or {@link #getAlias() alias} to match.
500
   * @return {@code true} if the given {@code nameOrAlias} is equal to {@link #getName() name} or {@link #getAlias() alias}, {@code false} otherwise.
501
   */
502
  public boolean matches(String nameOrAlias) {
503

504
    return this.name.equals(nameOrAlias) || Objects.equals(this.alias, nameOrAlias);
14✔
505
  }
506

507
  /**
508
   * @return {@code true} if this {@link Property} is valid, {@code false} if it is {@link #isRequired() required} but no {@link #getValue() value} has been
509
   *     set.
510
   * @throws RuntimeException if the {@link #getValue() value} is violating given constraints. This is checked by the optional {@link Consumer} function
511
   *     given at construction time.
512
   */
513
  public ValidationResult validate() {
514

515
    ValidationState state = new ValidationState(this.getNameOrAlias());
6✔
516

517
    if (this.required && (getValue() == null)) {
6!
518
      state.addErrorMessage("Value is required and cannot be empty.");
×
519
      return state;
×
520
    }
521
    if (this.validator != null) {
3!
522
      for (V value : this.value) {
×
523
        validator.validate(value, state);
×
524
      }
×
525
    }
526
    return state;
2✔
527
  }
528

529
  @Override
530
  public int hashCode() {
531

532
    return Objects.hash(this.name, this.value);
×
533
  }
534

535
  @Override
536
  public boolean equals(Object obj) {
537

538
    if (obj == this) {
3!
539
      return true;
2✔
540
    } else if ((obj == null) || (obj.getClass() != getClass())) {
×
541
      return false;
×
542
    }
543
    Property<?> other = (Property<?>) obj;
×
544
    if (!Objects.equals(this.name, other.name)) {
×
545
      return false;
×
546
    } else if (!Objects.equals(this.value, other.value)) {
×
547
      return false;
×
548
    }
549
    return true;
×
550
  }
551

552
  @Override
553
  public String toString() {
554

555
    StringBuilder sb = new StringBuilder();
×
556
    sb.append(getClass().getSimpleName());
×
557
    sb.append("[");
×
558
    if (this.name.isEmpty()) {
×
559
      sb.append(this.alias);
×
560
    } else {
561
      sb.append(this.name);
×
562
      if (this.alias != null) {
×
563
        sb.append(" | ");
×
564
        sb.append(this.alias);
×
565
      }
566
    }
567
    sb.append(":");
×
568
    sb.append(getValueAsString());
×
569
    sb.append("]");
×
570
    return sb.toString();
×
571
  }
572

573
}
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