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

devonfw / IDEasy / 26101908636

19 May 2026 01:55PM UTC coverage: 70.982% (+0.003%) from 70.979%
26101908636

Pull #1859

github

web-flow
Merge cf4a7f717 into b4eeee25f
Pull Request #1859: #1392: Smart completions

4472 of 6964 branches covered (64.22%)

Branch coverage included in aggregate %.

11521 of 15567 relevant lines covered (74.01%)

3.14 hits per line

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

71.9
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.CompletionCandidate;
15
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
16
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorAdapter;
17
import com.devonfw.tools.ide.context.IdeContext;
18
import com.devonfw.tools.ide.validation.PropertyValidator;
19
import com.devonfw.tools.ide.validation.ValidationResult;
20
import com.devonfw.tools.ide.validation.ValidationState;
21

22
/**
23
 * A {@link Property} is a simple container for a {@link #getValue() value} with a fixed {@link #getName() name} and {@link #getValueType() type}. Further we
24
 * 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
25
 * 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
26
 * {@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}
27
 * and {@link #getValueType() type}. Besides simplification this also prevents the use of reflection for generic CLI parsing with assigning and validating
28
 * arguments what is beneficial for compiling the Java code to a native image using GraalVM.
29
 *
30
 * @param <V> the {@link #getValueType() value type}.
31
 */
32
public abstract class Property<V> {
33

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

36
  private static final String INVALID_ARGUMENT = "Invalid CLI argument '{}' for property '{}' of commandlet '{}'";
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
  private final boolean placeholder;
55

56
  /** @see #getValue() */
57
  protected final List<V> value = new ArrayList<>();
5✔
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
    this(name, required, alias, false, null);
7✔
69
  }
1✔
70

71
  /**
72
   * The constructor.
73
   *
74
   * @param name the {@link #getName() property name}.
75
   * @param required the {@link #isRequired() required flag}.
76
   * @param alias the {@link #getAlias() property alias}.
77
   * @param multivalued the boolean flag about multiple arguments
78
   * @param validator the {@link Consumer} used to {@link #validate() validate} the {@link #getValue() value}.
79
   */
80
  public Property(String name, boolean required, String alias, boolean multivalued, PropertyValidator<V> validator) {
81
    this(name, required, alias, multivalued, false, validator);
8✔
82
  }
1✔
83

84
  /**
85
   * The constructor.
86
   *
87
   * @param name the {@link #getName() property name}.
88
   * @param required the {@link #isRequired() required flag}.
89
   * @param alias the {@link #getAlias() property alias}.
90
   * @param multivalued the boolean flag about multiple arguments
91
   * @param placeholder whether this property is substituted by some value or literal
92
   * @param validator the {@link Consumer} used to {@link #validate() validate} the {@link #getValue() value}.
93
   */
94
  public Property(String name, boolean required, String alias, boolean multivalued, boolean placeholder, PropertyValidator<V> validator) {
95
    super();
2✔
96

97
    this.name = name;
3✔
98
    this.required = required;
3✔
99
    this.alias = alias;
3✔
100
    this.multivalued = multivalued;
3✔
101
    this.placeholder = placeholder;
3✔
102
    this.validator = validator;
3✔
103
  }
1✔
104

105
  /**
106
   * @return the name of this property. Will be the empty {@link String} for a {@link #isValue() value} property that is not a keyword.
107
   */
108
  public String getName() {
109

110
    return this.name;
3✔
111
  }
112

113
  /**
114
   * @return the alias of this property or {@code null} for none.
115
   * @see #isOption()
116
   */
117
  public String getAlias() {
118

119
    return this.alias;
3✔
120
  }
121

122
  /**
123
   * @return the {@link #getName() name} or the {@link #getAlias() alias} if {@link #getName() name} is {@link String#isEmpty() empty}.
124
   */
125
  public String getNameOrAlias() {
126

127
    if (this.name.isEmpty()) {
4✔
128
      return this.alias;
3✔
129
    }
130
    return this.name;
3✔
131
  }
132

133
  /**
134
   * @return {@code true} if this property is required (if argument is not present the {@link Commandlet} cannot be invoked), {@code false} otherwise (if
135
   *     optional).
136
   */
137
  public boolean isRequired() {
138

139
    return this.required;
3✔
140
  }
141

142
  /**
143
   * @return {@code true} if a value is expected as additional CLI argument.
144
   */
145
  public boolean isExpectValue() {
146

147
    return "".equals(this.name);
×
148
  }
149

150
  /**
151
   * Determines if this {@link Property} is an option. Canonical options have a long-option {@link #getName() name} (e.g. "--force") and a short-option
152
   * {@link #getAlias() alias} (e.g. "-f").
153
   *
154
   * @return {@code true} if this {@link Property} is an option, {@code false} otherwise (if a positional argument).
155
   */
156
  public boolean isOption() {
157

158
    return this.name.startsWith("-");
5✔
159
  }
160

161
  /**
162
   * @return {@code true} if this {@link Property} forces an implicit {@link CliArgument#isEndOptions() end-options} as if "--" was provided before its first
163
   *     {@link CliArgument argument}.
164
   */
165
  public boolean isEndOptions() {
166

167
    return isMultiValued();
×
168
  }
169

170
  /**
171
   * 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}
172
   * of a {@link Commandlet}.
173
   *
174
   * @return {@code true} if multi-valued, {@code false} otherwise.
175
   */
176
  public boolean isMultiValued() {
177

178
    return this.multivalued;
3✔
179
  }
180

181
  public boolean isPlaceholder() {
182
    return this.placeholder;
3✔
183
  }
184

185
  /**
186
   * 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
187
   * 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}
188
   * is {@link String#isEmpty() empty} and the {@link #getAlias() alias} is a logical name of the value to display to users.
189
   *
190
   * @return {@code true} if value, {@code false} otherwise.
191
   */
192
  public boolean isValue() {
193

194
    return !isOption();
7✔
195
  }
196

197
  /**
198
   * @return the {@link Class} reflecting the type of the {@link #getValue() value}.
199
   */
200
  public abstract Class<V> getValueType();
201

202
  /**
203
   * @return the value of this property.
204
   * @see #setValue(Object)
205
   */
206
  public V getValue() {
207

208
    if (this.value.isEmpty()) {
4✔
209
      return null;
2✔
210
    } else {
211
      return this.value.getFirst();
4✔
212
    }
213
  }
214

215
  /**
216
   * @param i the position to get.
217
   * @return the value of this property.
218
   */
219
  public V getValue(int i) {
220

221
    return this.value.get(i);
5✔
222
  }
223

224
  /**
225
   * @return amount of values.
226
   */
227
  public int getValueCount() {
228

229
    return this.value.size();
4✔
230
  }
231

232
  /**
233
   * @return the {@link #getValue() value} as {@link String}.
234
   * @see #setValueAsString(String, IdeContext)
235
   */
236
  public String getValueAsString() {
237

238
    if (getValue() == null) {
3!
239
      return null;
×
240
    }
241
    return format(getValue());
5✔
242
  }
243

244
  /**
245
   * @return a {@link List} containing all {@link #getValue(int) values}. This method only makes sense for {@link #isMultiValued() multi valued} properties.
246
   */
247
  public List<V> asList() {
248

249
    return new ArrayList<>(this.value);
6✔
250
  }
251

252
  /**
253
   * @param valueToFormat the value to format.
254
   * @return the given {@code value} formatted as {@link String}.
255
   */
256
  protected String format(V valueToFormat) {
257

258
    return valueToFormat.toString();
3✔
259
  }
260

261
  /**
262
   * @param value the new {@link #getValue() value} to set.
263
   * @see #getValue()
264
   */
265
  public void setValue(V value) {
266

267
    if (!this.multivalued) {
3✔
268
      this.value.clear();
3✔
269
    }
270
    this.value.add(value);
5✔
271
  }
1✔
272

273
  /**
274
   * Clears the {@link #value value} list.
275
   */
276
  public void clearValue() {
277

278
    this.value.clear();
3✔
279
  }
1✔
280

281
  /**
282
   * @param value the value to add to the {@link List} of values.
283
   * @see #isMultiValued()
284
   */
285
  public void addValue(V value) {
286

287
    if (!this.multivalued) {
3!
288
      throw new IllegalStateException("not multivalued");
×
289
    }
290
    this.value.add(value);
5✔
291
  }
1✔
292

293
  /**
294
   * @param value the new {@link #getValue() value} to set.
295
   * @param i the position to set.
296
   */
297
  public void setValue(V value, int i) {
298

299
    this.value.set(i, value);
6✔
300
  }
1✔
301

302
  /**
303
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
304
   * @param context the {@link IdeContext}
305
   * @see #getValueAsString()
306
   */
307
  public void setValueAsString(String valueAsString, IdeContext context) {
308

309
    if (valueAsString == null) {
2!
310
      setValue(getNullValue());
×
311
    } else {
312
      setValue(parse(valueAsString, context));
6✔
313
    }
314
  }
1✔
315

316
  /**
317
   * Like {@link #setValueAsString(String, IdeContext)} but with exception handling.
318
   *
319
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
320
   * @param context the {@link IdeContext}
321
   * @param commandlet the {@link Commandlet} owning this property.
322
   * @return {@code true} if the value has been assigned successfully, {@code false} otherwise (an error occurred).
323
   */
324
  public final boolean assignValueAsString(String valueAsString, IdeContext context, Commandlet commandlet) {
325

326
    try {
327
      setValueAsString(valueAsString, context);
4✔
328
      return true;
2✔
329
    } catch (Exception e) {
×
330
      if (e instanceof IllegalArgumentException) {
×
331
        LOG.warn(INVALID_ARGUMENT, valueAsString, getNameOrAlias(), commandlet.getName());
×
332
      } else {
333
        LOG.warn(INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE, valueAsString, getNameOrAlias(), commandlet.getName(), e.getMessage());
×
334
      }
335
      return false;
×
336
    }
337
  }
338

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

344
    return null;
×
345
  }
346

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

354
  /**
355
   * @param args the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
356
   * @param context the {@link IdeContext}.
357
   * @param commandlet the {@link Commandlet} owning this property.
358
   * @param collector the {@link CompletionCandidateCollector}.
359
   * @return {@code true} if it matches, {@code false} otherwise.
360
   */
361
  public boolean apply(CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
362
    if (this.placeholder && args.current().isCompletion()) {
7!
363
      return false;
2✔
364
    }
365
    boolean match = this.apply(this.name, args, context, commandlet, collector);
9✔
366

367
    if (args.current().isCompletion() && this.alias != null && !this.name.isEmpty()) {
11✔
368
      match |= this.apply(this.alias, args, context, commandlet, collector);
11✔
369
    }
370

371
    return match;
2✔
372
  }
373

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

384
    CliArgument argument = args.current();
3✔
385
    if (argument.isCompletion()) {
3✔
386
      if (collector == null) {
2✔
387
        return false;
2✔
388
      }
389

390
      int size = collector.getCandidates().size();
4✔
391
      boolean match = this.complete(normalizedName, argument, args, context, commandlet, collector);
9✔
392

393
      return match;
2✔
394
    }
395

396
    boolean option = normalizedName.startsWith("-");
4✔
397
    if (option && !argument.isOption() || !option && argument.isOption()
11✔
398
        && argument.get().length() > 0 && !args.isEndOptions()) {
6!
399
      return false;
2✔
400
    }
401

402
    String argValue = null;
2✔
403
    boolean lookahead = false;
2✔
404

405
    if (normalizedName.isEmpty()) {
3✔
406
      argValue = argument.get();
4✔
407
    } else {
408
      if (!matches(argument.getKey())) {
5✔
409
        return false;
2✔
410
      }
411
      argValue = argument.getValue();
3✔
412
      if (argValue == null) {
2!
413
        if (argument.isCompletion()) {
3!
414
          completeValue(argument.get(), context, commandlet, collector);
×
415
          return true;
×
416
        } else {
417
          if (!argument.isEnd()) {
3!
418
            argValue = argument.get();
3✔
419
          }
420
          lookahead = true;
2✔
421
        }
422
      }
423
    }
424
    return applyValue(argValue, lookahead, args, context, commandlet, collector);
9✔
425
  }
426

427
  /**
428
   * @param argValue the value to set as {@link String}.
429
   * @param lookahead - {@code true} if the given {@code argValue} is taken as lookahead from the next value, {@code false} otherwise.
430
   * @param args the {@link CliArguments}.
431
   * @param context the {@link IdeContext}.
432
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
433
   * @param collector the {@link CompletionCandidateCollector}.
434
   * @return {@code true} if it matches, {@code false} otherwise.
435
   */
436
  protected boolean applyValue(String argValue, boolean lookahead, CliArguments args, IdeContext context, Commandlet commandlet,
437
      CompletionCandidateCollector collector) {
438

439
    boolean success = assignValueAsString(argValue, context, commandlet);
6✔
440
    return success;
2✔
441
  }
442

443
  /**
444
   * Performs auto-completion for the {@code arg}.
445
   *
446
   * @param normalizedName the {@link #getName() name} or potentially a normalized form of it (see {@link KeywordProperty}).
447
   * @param argument the {@link CliArgument CLI argument}.
448
   * @param args the {@link CliArguments}.
449
   * @param context the {@link IdeContext}.
450
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
451
   * @param collector the {@link CompletionCandidateCollector}.
452
   * @return {@code true} if completion succeeded, {@code false} otherwise
453
   */
454
  protected boolean complete(
455
      String normalizedName, CliArgument argument, CliArguments args, IdeContext context,
456
      Commandlet commandlet, CompletionCandidateCollector collector
457
  ) {
458
    String arg = argument.get();
3✔
459

460
    if (normalizedName.isEmpty()) {
3✔
461
      int count = collector.getCandidates().size();
4✔
462
      completeValue(arg, context, commandlet, collector);
6✔
463

464
      if (collector.getCandidates().size() > count) {
5✔
465
        return true;
2✔
466
      }
467

468
      return false;
2✔
469
    }
470

471
    if (normalizedName.startsWith(arg)) {
4✔
472
      boolean complete = !normalizedName.endsWith("=");
8✔
473
      CompletionCandidate candidate = collector.createCandidate(normalizedName, null, complete);
6✔
474

475
      collector.add(candidate);
3✔
476
      return true;
2✔
477
    }
478

479
    if (this.alias != null) {
3✔
480
      if (this.alias.length() == 2 && this.alias.charAt(0) == '-' && argument.isShortOption()) {
14!
481
        char opt = this.alias.charAt(1);
5✔
482
        if (arg.indexOf(opt) < 0) {
4✔
483
          collector.add(arg + opt, null);
6✔
484
        }
485
      }
486
    }
487

488
    return false;
2✔
489
  }
490

491
  /**
492
   * Performs auto-completion for the {@code arg} as {@link #getValue() property value}.
493
   *
494
   * @param arg the {@link CliArgument#get() CLI argument}.
495
   * @param context the {@link IdeContext}.
496
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
497
   * @param collector the {@link CompletionCandidateCollector}.
498
   */
499
  protected abstract void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector);
500

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

507
    return this.name.equals(nameOrAlias) || Objects.equals(this.alias, nameOrAlias);
14✔
508
  }
509

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

518
    ValidationState state = new ValidationState(this.getNameOrAlias());
6✔
519

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

532
  @Override
533
  public int hashCode() {
534

535
    return Objects.hash(this.name, this.value);
×
536
  }
537

538
  @Override
539
  public boolean equals(Object obj) {
540

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

555
  @Override
556
  public String toString() {
557

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

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