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

devonfw / IDEasy / 24984011544

27 Apr 2026 08:14AM UTC coverage: 70.718% (+0.08%) from 70.641%
24984011544

Pull #1856

github

web-flow
Merge f59d7af1d into 344d6c0f7
Pull Request #1856: #1643 improve ux on syntax error

4403 of 6878 branches covered (64.02%)

Branch coverage included in aggregate %.

11348 of 15395 relevant lines covered (73.71%)

3.12 hits per line

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

73.0
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
  public static final String INVALID_ARGUMENT = "Invalid CLI argument '{}' for property '{}' of commandlet '{}'";
36
  public static final String INVALID_ARGUMENT_HELP_MULTIPLE = "Did you mean one of [{}]?";
37

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

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

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

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

49
  private final PropertyValidator<V> validator;
50

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

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

57
  private String lastParseHint;
58

59
  private String lastParseExceptionMessage;
60

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

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

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

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

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

102
    return this.name;
3✔
103
  }
104

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

111
    return this.alias;
3✔
112
  }
113

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

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

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

131
    return this.required;
3✔
132
  }
133

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

139
    return "".equals(this.name);
×
140
  }
141

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

150
    return this.name.startsWith("-");
5✔
151
  }
152

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

159
    return isMultiValued();
×
160
  }
161

162
  /**
163
   * 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}
164
   * of a {@link Commandlet}.
165
   *
166
   * @return {@code true} if multi-valued, {@code false} otherwise.
167
   */
168
  public boolean isMultiValued() {
169

170
    return this.multivalued;
3✔
171
  }
172

173
  /**
174
   * 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
175
   * 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}
176
   * is {@link String#isEmpty() empty} and the {@link #getAlias() alias} is a logical name of the value to display to users.
177
   *
178
   * @return {@code true} if value, {@code false} otherwise.
179
   */
180
  public boolean isValue() {
181

182
    return !isOption();
7✔
183
  }
184

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

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

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

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

209
    return this.value.get(i);
5✔
210
  }
211

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

217
    return this.value.size();
4✔
218
  }
219

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

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

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

237
    return new ArrayList<>(this.value);
6✔
238
  }
239

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

246
    return valueToFormat.toString();
3✔
247
  }
248

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

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

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

266
    this.value.clear();
3✔
267
  }
1✔
268

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

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

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

287
    this.value.set(i, value);
6✔
288
  }
1✔
289

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

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

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

314
    this.lastParseHint = null;
3✔
315
    this.lastParseExceptionMessage = null;
3✔
316
    try {
317
      setValueAsString(valueAsString, context);
4✔
318
      return true;
2✔
319
    } catch (Exception e) {
1✔
320
      if (!(e instanceof IllegalArgumentException)) {
3!
321
        this.lastParseExceptionMessage = e.getMessage();
×
322
      }
323
      this.lastParseHint = getValidValuesErrorHint(context, commandlet);
6✔
324
      return false;
2✔
325
    }
326
  }
327

328
  /**
329
   * @return the hint string for the last failed parse (e.g. "Did you mean one of [...]?"), and clears it. Returns {@code null} if no hint is available.
330
   */
331
  public String getAndClearLastParseHint() {
332

333
    String h = this.lastParseHint;
3✔
334
    this.lastParseHint = null;
3✔
335
    return h;
2✔
336
  }
337

338
  /**
339
   * @return the exception message from the last failed parse if the exception was not an {@link IllegalArgumentException}, and clears it. Returns {@code null}
340
   *     otherwise.
341
   */
342
  public String getAndClearLastParseExceptionMessage() {
343

344
    String m = this.lastParseExceptionMessage;
3✔
345
    this.lastParseExceptionMessage = null;
3✔
346
    return m;
2✔
347
  }
348

349
  /**
350
   * @param context the {@link IdeContext}.
351
   * @param commandlet the {@link Commandlet} owning this property.
352
   * @return a formatted string of valid values to show as a hint when an invalid value is given, or {@code null} if no hint is available.
353
   */
354
  protected String getValidValuesErrorHint(IdeContext context, Commandlet commandlet) {
355

356
    return null;
×
357
  }
358

359
  /**
360
   * @return the {@code null} value.
361
   */
362
  protected V getNullValue() {
363

364
    return null;
×
365
  }
366

367
  /**
368
   * @param valueAsString the value to parse given as {@link String}.
369
   * @param context the {@link IdeContext}.
370
   * @return the parsed value.
371
   */
372
  public abstract V parse(String valueAsString, IdeContext context);
373

374
  /**
375
   * @param args the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
376
   * @param context the {@link IdeContext}.
377
   * @param commandlet the {@link Commandlet} owning this property.
378
   * @param collector the {@link CompletionCandidateCollector}.
379
   * @return {@code true} if it matches, {@code false} otherwise.
380
   */
381
  public boolean apply(CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
382

383
    return apply(this.name, args, context, commandlet, collector);
9✔
384
  }
385

386
  /**
387
   * @param normalizedName the {@link #getName() name} or potentially a normalized form of it (see {@link KeywordProperty}).
388
   * @param args the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
389
   * @param context the {@link IdeContext}.
390
   * @param commandlet the {@link Commandlet} owning this property.
391
   * @param collector the {@link CompletionCandidateCollector}.
392
   * @return {@code true} if it matches, {@code false} otherwise.
393
   */
394
  protected boolean apply(String normalizedName, CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
395

396
    CliArgument argument = args.current();
3✔
397
    if (argument.isCompletion()) {
3✔
398
      int size = collector.getCandidates().size();
4✔
399
      complete(normalizedName, argument, args, context, commandlet, collector);
8✔
400
      return (collector.getCandidates().size() > size);
9✔
401
    }
402
    boolean option = normalizedName.startsWith("-");
4✔
403
    if (option && !argument.isOption()) {
5✔
404
      return false;
2✔
405
    }
406
    if (!option && argument.isOption() && (argument.get().length() > 1) && args.isSplitShortOpts()) {
13✔
407
      return false;
2✔
408
    }
409
    String argValue = null;
2✔
410
    boolean lookahead = false;
2✔
411
    if (normalizedName.isEmpty()) {
3✔
412
      argValue = argument.get();
4✔
413
    } else {
414
      if (!matches(argument.getKey())) {
5✔
415
        return false;
2✔
416
      }
417
      argValue = argument.getValue();
3✔
418
      if (argValue == null) {
2✔
419
        argument = args.next();
3✔
420
        if (argument.isCompletion()) {
3✔
421
          completeValue(argument.get(), context, commandlet, collector);
7✔
422
          return true;
2✔
423
        } else {
424
          if (!argument.isEnd()) {
3✔
425
            argValue = argument.get();
3✔
426
          }
427
          lookahead = true;
2✔
428
        }
429
      }
430
    }
431
    return applyValue(argValue, lookahead, args, context, commandlet, collector);
9✔
432
  }
433

434
  /**
435
   * @param argValue the value to set as {@link String}.
436
   * @param lookahead - {@code true} if the given {@code argValue} is taken as lookahead from the next value, {@code false} otherwise.
437
   * @param args the {@link CliArguments}.
438
   * @param context the {@link IdeContext}.
439
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
440
   * @param collector the {@link CompletionCandidateCollector}.
441
   * @return {@code true} if it matches, {@code false} otherwise.
442
   */
443
  protected boolean applyValue(String argValue, boolean lookahead, CliArguments args, IdeContext context, Commandlet commandlet,
444
      CompletionCandidateCollector collector) {
445

446
    boolean success = assignValueAsString(argValue, context, commandlet);
6✔
447

448
    if (success) {
2✔
449
      if (this.multivalued) {
3✔
450
        while (success && args.hasNext()) {
5!
451
          CliArgument arg = args.next();
×
452
          success = assignValueAsString(arg.get(), context, commandlet);
×
453
        }
×
454
      }
455
    }
456
    args.next();
3✔
457
    return success;
2✔
458
  }
459

460
  /**
461
   * Performs auto-completion for the {@code arg}.
462
   *
463
   * @param normalizedName the {@link #getName() name} or potentially a normalized form of it (see {@link KeywordProperty}).
464
   * @param argument the {@link CliArgument CLI argument}.
465
   * @param args the {@link CliArguments}.
466
   * @param context the {@link IdeContext}.
467
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
468
   * @param collector the {@link CompletionCandidateCollector}.
469
   */
470
  protected void complete(String normalizedName, CliArgument argument, CliArguments args, IdeContext context, Commandlet commandlet,
471
      CompletionCandidateCollector collector) {
472

473
    String arg = argument.get();
3✔
474
    if (normalizedName.isEmpty()) {
3✔
475
      int count = collector.getCandidates().size();
4✔
476
      completeValue(arg, context, commandlet, collector);
6✔
477
      if (collector.getCandidates().size() > count) {
5✔
478
        args.next();
3✔
479
      }
480
      return;
1✔
481
    }
482
    if (normalizedName.startsWith(arg)) {
4✔
483
      collector.add(normalizedName, null, this, commandlet);
6✔
484
    }
485
    if (this.alias != null) {
3✔
486
      if (this.alias.startsWith(arg)) {
5✔
487
        collector.add(this.alias, null, this, commandlet);
8✔
488
      } else if ((this.alias.length() == 2) && (this.alias.charAt(0) == '-') && argument.isShortOption()) {
14!
489
        char opt = this.alias.charAt(1); // e.g. arg="-do" and alias="-f" -complete-> "-dof"
5✔
490
        if (arg.indexOf(opt) < 0) {
4✔
491
          collector.add(arg + opt, null, this, commandlet);
8✔
492
        }
493
      }
494
    }
495
    String value = argument.getValue();
3✔
496
    if (value != null) {
2!
497
      String key = argument.getKey();
×
498
      if (normalizedName.equals(key) || Objects.equals(this.alias, key)) {
×
499
        completeValue(value, context, commandlet, new CompletionCandidateCollectorAdapter(key + "=", collector));
×
500
      }
501
    }
502
  }
1✔
503

504
  /**
505
   * Performs auto-completion for the {@code arg} as {@link #getValue() property value}.
506
   *
507
   * @param arg the {@link CliArgument#get() CLI argument}.
508
   * @param context the {@link IdeContext}.
509
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
510
   * @param collector the {@link CompletionCandidateCollector}.
511
   */
512
  protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
513

514
  }
1✔
515

516
  /**
517
   * @param nameOrAlias the potential {@link #getName() name} or {@link #getAlias() alias} to match.
518
   * @return {@code true} if the given {@code nameOrAlias} is equal to {@link #getName() name} or {@link #getAlias() alias}, {@code false} otherwise.
519
   */
520
  public boolean matches(String nameOrAlias) {
521

522
    return this.name.equals(nameOrAlias) || Objects.equals(this.alias, nameOrAlias);
14✔
523
  }
524

525
  /**
526
   * @return {@code true} if this {@link Property} is valid, {@code false} if it is {@link #isRequired() required} but no {@link #getValue() value} has been
527
   *     set.
528
   * @throws RuntimeException if the {@link #getValue() value} is violating given constraints. This is checked by the optional {@link Consumer} function
529
   *     given at construction time.
530
   */
531
  public ValidationResult validate() {
532

533
    ValidationState state = new ValidationState(this.getNameOrAlias());
6✔
534

535
    if (this.required && (getValue() == null)) {
6!
536
      state.addErrorMessage("Value is required and cannot be empty.");
×
537
      return state;
×
538
    }
539
    if (this.validator != null) {
3!
540
      for (V value : this.value) {
×
541
        validator.validate(value, state);
×
542
      }
×
543
    }
544
    return state;
2✔
545
  }
546

547
  @Override
548
  public int hashCode() {
549

550
    return Objects.hash(this.name, this.value);
×
551
  }
552

553
  @Override
554
  public boolean equals(Object obj) {
555

556
    if (obj == this) {
3!
557
      return true;
2✔
558
    } else if ((obj == null) || (obj.getClass() != getClass())) {
×
559
      return false;
×
560
    }
561
    Property<?> other = (Property<?>) obj;
×
562
    if (!Objects.equals(this.name, other.name)) {
×
563
      return false;
×
564
    } else if (!Objects.equals(this.value, other.value)) {
×
565
      return false;
×
566
    }
567
    return true;
×
568
  }
569

570
  @Override
571
  public String toString() {
572

573
    StringBuilder sb = new StringBuilder();
×
574
    sb.append(getClass().getSimpleName());
×
575
    sb.append("[");
×
576
    if (this.name.isEmpty()) {
×
577
      sb.append(this.alias);
×
578
    } else {
579
      sb.append(this.name);
×
580
      if (this.alias != null) {
×
581
        sb.append(" | ");
×
582
        sb.append(this.alias);
×
583
      }
584
    }
585
    sb.append(":");
×
586
    sb.append(getValueAsString());
×
587
    sb.append("]");
×
588
    return sb.toString();
×
589
  }
590

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