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

devonfw / IDEasy / 17820842601

18 Sep 2025 07:05AM UTC coverage: 68.539% (-0.08%) from 68.618%
17820842601

push

github

web-flow
#1491: fix reuse software from repo (#1495)

3420 of 5461 branches covered (62.63%)

Branch coverage included in aggregate %.

8919 of 12542 relevant lines covered (71.11%)

3.12 hits per line

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

75.49
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 com.devonfw.tools.ide.cli.CliArgument;
9
import com.devonfw.tools.ide.cli.CliArguments;
10
import com.devonfw.tools.ide.commandlet.Commandlet;
11
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
12
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorAdapter;
13
import com.devonfw.tools.ide.context.IdeContext;
14
import com.devonfw.tools.ide.validation.PropertyValidator;
15
import com.devonfw.tools.ide.validation.ValidationResult;
16
import com.devonfw.tools.ide.validation.ValidationState;
17

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

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

32
  private static final String INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE = INVALID_ARGUMENT + ": {}";
33

34
  /** @see #getName() */
35
  protected final String name;
36

37
  /** @see #getAlias() */
38
  protected final String alias;
39

40
  /** @see #isRequired() */
41
  protected final boolean required;
42

43
  private final PropertyValidator<V> validator;
44

45
  /** @see #isMultiValued() */
46
  private final boolean multivalued;
47

48
  /** @see #getValue() */
49
  protected final List<V> value = new ArrayList<>();
10✔
50

51
  /**
52
   * The constructor.
53
   *
54
   * @param name the {@link #getName() property name}.
55
   * @param required the {@link #isRequired() required flag}.
56
   * @param alias the {@link #getAlias() property alias}.
57
   */
58
  public Property(String name, boolean required, String alias) {
59

60
    super();
2✔
61
    this.name = name;
3✔
62
    this.required = required;
3✔
63
    this.alias = alias;
3✔
64
    this.multivalued = false;
3✔
65
    this.validator = null;
3✔
66
  }
1✔
67

68
  /**
69
   * The constructor.
70
   *
71
   * @param name the {@link #getName() property name}.
72
   * @param required the {@link #isRequired() required flag}.
73
   * @param alias the {@link #getAlias() property alias}.
74
   * @param multivalued the boolean flag about multiple arguments
75
   * @param validator the {@link Consumer} used to {@link #validate() validate} the {@link #getValue() value}.
76
   */
77
  public Property(String name, boolean required, String alias, boolean multivalued, PropertyValidator<V> validator) {
78

79
    super();
2✔
80
    this.name = name;
3✔
81
    this.required = required;
3✔
82
    this.alias = alias;
3✔
83
    this.validator = validator;
3✔
84
    this.multivalued = multivalued;
3✔
85
  }
1✔
86

87
  /**
88
   * @return the name of this property. Will be the empty {@link String} for a {@link #isValue() value} property that is not a keyword.
89
   */
90
  public String getName() {
91

92
    return this.name;
3✔
93
  }
94

95
  /**
96
   * @return the alias of this property or {@code null} for none.
97
   * @see #isOption()
98
   */
99
  public String getAlias() {
100

101
    return this.alias;
3✔
102
  }
103

104
  /**
105
   * @return the {@link #getName() name} or the {@link #getAlias() alias} if {@link #getName() name} is {@link String#isEmpty() empty}.
106
   */
107
  public String getNameOrAlias() {
108

109
    if (this.name.isEmpty()) {
4✔
110
      return this.alias;
3✔
111
    }
112
    return this.name;
3✔
113
  }
114

115
  /**
116
   * @return {@code true} if this property is required (if argument is not present the {@link Commandlet} cannot be invoked), {@code false} otherwise (if
117
   *     optional).
118
   */
119
  public boolean isRequired() {
120

121
    return this.required;
3✔
122
  }
123

124
  /**
125
   * @return {@code true} if a value is expected as additional CLI argument.
126
   */
127
  public boolean isExpectValue() {
128

129
    return "".equals(this.name);
×
130
  }
131

132
  /**
133
   * Determines if this {@link Property} is an option. Canonical options have a long-option {@link #getName() name} (e.g. "--force") and a short-option
134
   * {@link #getAlias() alias} (e.g. "-f").
135
   *
136
   * @return {@code true} if this {@link Property} is an option, {@code false} otherwise (if a positional argument).
137
   */
138
  public boolean isOption() {
139

140
    return this.name.startsWith("-");
5✔
141
  }
142

143
  /**
144
   * @return {@code true} if this {@link Property} forces an implicit {@link CliArgument#isEndOptions() end-options} as if "--" was provided before its first
145
   *     {@link CliArgument argument}.
146
   */
147
  public boolean isEndOptions() {
148

149
    return isMultiValued();
×
150
  }
151

152
  /**
153
   * 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}
154
   * of a {@link Commandlet}.
155
   *
156
   * @return {@code true} if multi-valued, {@code false} otherwise.
157
   */
158
  public boolean isMultiValued() {
159

160
    return this.multivalued;
3✔
161
  }
162

163
  /**
164
   * 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
165
   * 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}
166
   * is {@link String#isEmpty() empty} and the {@link #getAlias() alias} is a logical name of the value to display to users.
167
   *
168
   * @return {@code true} if value, {@code false} otherwise.
169
   */
170
  public boolean isValue() {
171

172
    return !isOption();
7✔
173
  }
174

175
  /**
176
   * @return the {@link Class} reflecting the type of the {@link #getValue() value}.
177
   */
178
  public abstract Class<V> getValueType();
179

180
  /**
181
   * @return the value of this property.
182
   * @see #setValue(Object)
183
   */
184
  public V getValue() {
185

186
    if (this.value.isEmpty()) {
4✔
187
      return null;
2✔
188
    } else {
189
      return this.value.get(0);
5✔
190
    }
191
  }
192

193
  /**
194
   * @param i the position to get.
195
   * @return the value of this property.
196
   */
197
  public V getValue(int i) {
198

199
    return this.value.get(i);
5✔
200
  }
201

202
  /**
203
   * @return amount of values.
204
   */
205
  public int getValueCount() {
206

207
    return this.value.size();
4✔
208
  }
209

210
  /**
211
   * @return the {@link #getValue() value} as {@link String}.
212
   * @see #setValueAsString(String, IdeContext)
213
   */
214
  public String getValueAsString() {
215

216
    if (getValue() == null) {
3!
217
      return null;
2✔
218
    }
219
    return format(getValue());
×
220
  }
221

222
  /**
223
   * @return a {@link List} containing all {@link #getValue(int) values}. This method only makes sense for {@link #isMultiValued() multi valued} properties.
224
   */
225
  public List<V> asList() {
226

227
    return new ArrayList<>(this.value);
×
228
  }
229

230
  /**
231
   * @param valueToFormat the value to format.
232
   * @return the given {@code value} formatted as {@link String}.
233
   */
234
  protected String format(V valueToFormat) {
235

236
    return valueToFormat.toString();
×
237
  }
238

239
  /**
240
   * @param value the new {@link #getValue() value} to set.
241
   * @see #getValue()
242
   */
243
  public void setValue(V value) {
244

245
    if (!this.multivalued) {
3✔
246
      this.value.clear();
3✔
247
    }
248
    this.value.add(value);
5✔
249
  }
1✔
250

251
  /**
252
   * Clears the {@link #value value} list.
253
   */
254
  public void clearValue() {
255

256
    this.value.clear();
3✔
257
  }
1✔
258

259
  /**
260
   * @param value the value to add to the {@link List} of values.
261
   * @see #isMultiValued()
262
   */
263
  public void addValue(V value) {
264

265
    if (!this.multivalued) {
3!
266
      throw new IllegalStateException("not multivalued");
×
267
    }
268
    this.value.add(value);
5✔
269
  }
1✔
270

271
  /**
272
   * @param value the new {@link #getValue() value} to set.
273
   * @param i the position to set.
274
   */
275
  public void setValue(V value, int i) {
276

277
    this.value.set(i, value);
6✔
278
  }
1✔
279

280
  /**
281
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
282
   * @param context the {@link IdeContext}
283
   * @see #getValueAsString()
284
   */
285
  public void setValueAsString(String valueAsString, IdeContext context) {
286

287
    if (valueAsString == null) {
2!
288
      setValue(getNullValue());
×
289
    } else {
290
      setValue(parse(valueAsString, context));
6✔
291
    }
292
  }
1✔
293

294
  /**
295
   * Like {@link #setValueAsString(String, IdeContext)} but with exception handling.
296
   *
297
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
298
   * @param context the {@link IdeContext}
299
   * @param commandlet the {@link Commandlet} owning this property.
300
   * @return {@code true} if the value has been assigned successfully, {@code false} otherwise (an error occurred).
301
   */
302
  public final boolean assignValueAsString(String valueAsString, IdeContext context, Commandlet commandlet) {
303

304
    try {
305
      setValueAsString(valueAsString, context);
4✔
306
      return true;
2✔
307
    } catch (Exception e) {
×
308
      if (e instanceof IllegalArgumentException) {
×
309
        context.warning(INVALID_ARGUMENT, valueAsString, getNameOrAlias(), commandlet.getName());
×
310
      } else {
311
        context.warning(INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE, valueAsString, getNameOrAlias(), commandlet.getName(), e.getMessage());
×
312
      }
313
      return false;
×
314
    }
315
  }
316

317
  /**
318
   * @return the {@code null} value.
319
   */
320
  protected V getNullValue() {
321

322
    return null;
×
323
  }
324

325
  /**
326
   * @param valueAsString the value to parse given as {@link String}.
327
   * @param context the {@link IdeContext}.
328
   * @return the parsed value.
329
   */
330
  public abstract V parse(String valueAsString, IdeContext context);
331

332
  /**
333
   * @param args the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
334
   * @param context the {@link IdeContext}.
335
   * @param commandlet the {@link Commandlet} owning this property.
336
   * @param collector the {@link CompletionCandidateCollector}.
337
   * @return {@code true} if it matches, {@code false} otherwise.
338
   */
339
  public boolean apply(CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
340

341
    return apply(this.name, args, context, commandlet, collector);
9✔
342
  }
343

344
  /**
345
   * @param normalizedName the {@link #getName() name} or potentially a normalized form of it (see {@link KeywordProperty}).
346
   * @param args the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
347
   * @param context the {@link IdeContext}.
348
   * @param commandlet the {@link Commandlet} owning this property.
349
   * @param collector the {@link CompletionCandidateCollector}.
350
   * @return {@code true} if it matches, {@code false} otherwise.
351
   */
352
  protected boolean apply(String normalizedName, CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
353

354
    CliArgument argument = args.current();
3✔
355
    if (argument.isCompletion()) {
3✔
356
      int size = collector.getCandidates().size();
4✔
357
      complete(normalizedName, argument, args, context, commandlet, collector);
8✔
358
      return (collector.getCandidates().size() > size);
9✔
359
    }
360
    boolean option = normalizedName.startsWith("-");
4✔
361
    if (option && !argument.isOption()) {
5✔
362
      return false;
2✔
363
    }
364
    if (!option && argument.isOption() && (argument.get().length() > 1) && args.isSplitShortOpts()) {
13✔
365
      return false;
2✔
366
    }
367
    String argValue = null;
2✔
368
    boolean lookahead = false;
2✔
369
    if (normalizedName.isEmpty()) {
3✔
370
      argValue = argument.get();
4✔
371
    } else {
372
      if (!matches(argument.getKey())) {
5✔
373
        return false;
2✔
374
      }
375
      argValue = argument.getValue();
3✔
376
      if (argValue == null) {
2!
377
        argument = args.next();
3✔
378
        if (argument.isCompletion()) {
3✔
379
          completeValue(argument.get(), context, commandlet, collector);
7✔
380
          return true;
2✔
381
        } else {
382
          if (!argument.isEnd()) {
3✔
383
            argValue = argument.get();
3✔
384
          }
385
          lookahead = true;
2✔
386
        }
387
      }
388
    }
389
    return applyValue(argValue, lookahead, args, context, commandlet, collector);
9✔
390
  }
391

392
  /**
393
   * @param argValue the value to set as {@link String}.
394
   * @param lookahead - {@code true} if the given {@code argValue} is taken as lookahead from the next value, {@code false} otherwise.
395
   * @param args the {@link CliArguments}.
396
   * @param context the {@link IdeContext}.
397
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
398
   * @param collector the {@link CompletionCandidateCollector}.
399
   * @return {@code true} if it matches, {@code false} otherwise.
400
   */
401
  protected boolean applyValue(String argValue, boolean lookahead, CliArguments args, IdeContext context, Commandlet commandlet,
402
      CompletionCandidateCollector collector) {
403

404
    boolean success = assignValueAsString(argValue, context, commandlet);
6✔
405

406
    if (success) {
2!
407
      if (this.multivalued) {
3✔
408
        while (success && args.hasNext()) {
5!
409
          CliArgument arg = args.next();
×
410
          success = assignValueAsString(arg.get(), context, commandlet);
×
411
        }
×
412
      }
413
    }
414
    args.next();
3✔
415
    return success;
2✔
416
  }
417

418
  /**
419
   * Performs auto-completion for the {@code arg}.
420
   *
421
   * @param normalizedName the {@link #getName() name} or potentially a normalized form of it (see {@link KeywordProperty}).
422
   * @param argument the {@link CliArgument CLI argument}.
423
   * @param args the {@link CliArguments}.
424
   * @param context the {@link IdeContext}.
425
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
426
   * @param collector the {@link CompletionCandidateCollector}.
427
   */
428
  protected void complete(String normalizedName, CliArgument argument, CliArguments args, IdeContext context, Commandlet commandlet,
429
      CompletionCandidateCollector collector) {
430

431
    String arg = argument.get();
3✔
432
    if (normalizedName.isEmpty()) {
3✔
433
      int count = collector.getCandidates().size();
4✔
434
      completeValue(arg, context, commandlet, collector);
6✔
435
      if (collector.getCandidates().size() > count) {
5✔
436
        args.next();
3✔
437
      }
438
      return;
1✔
439
    }
440
    if (normalizedName.startsWith(arg)) {
4✔
441
      collector.add(normalizedName, null, this, commandlet);
6✔
442
    }
443
    if (this.alias != null) {
3✔
444
      if (this.alias.startsWith(arg)) {
5✔
445
        collector.add(this.alias, null, this, commandlet);
8✔
446
      } else if ((this.alias.length() == 2) && (this.alias.charAt(0) == '-') && argument.isShortOption()) {
14!
447
        char opt = this.alias.charAt(1); // e.g. arg="-do" and alias="-f" -complete-> "-dof"
5✔
448
        if (arg.indexOf(opt) < 0) {
4✔
449
          collector.add(arg + opt, null, this, commandlet);
8✔
450
        }
451
      }
452
    }
453
    String value = argument.getValue();
3✔
454
    if (value != null) {
2!
455
      String key = argument.getKey();
×
456
      if (normalizedName.equals(key) || Objects.equals(this.alias, key)) {
×
457
        completeValue(value, context, commandlet, new CompletionCandidateCollectorAdapter(key + "=", collector));
×
458
      }
459
    }
460
  }
1✔
461

462
  /**
463
   * Performs auto-completion for the {@code arg} as {@link #getValue() property value}.
464
   *
465
   * @param arg the {@link CliArgument#get() CLI argument}.
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 completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
471

472
  }
1✔
473

474
  /**
475
   * @param nameOrAlias the potential {@link #getName() name} or {@link #getAlias() alias} to match.
476
   * @return {@code true} if the given {@code nameOrAlias} is equal to {@link #getName() name} or {@link #getAlias() alias}, {@code false} otherwise.
477
   */
478
  public boolean matches(String nameOrAlias) {
479

480
    return this.name.equals(nameOrAlias) || Objects.equals(this.alias, nameOrAlias);
14✔
481
  }
482

483
  /**
484
   * @return {@code true} if this {@link Property} is valid, {@code false} if it is {@link #isRequired() required} but no {@link #getValue() value} has been
485
   *     set.
486
   * @throws RuntimeException if the {@link #getValue() value} is violating given constraints. This is checked by the optional {@link Consumer} function
487
   *     given at construction time.
488
   */
489
  public ValidationResult validate() {
490

491
    ValidationState state = new ValidationState(this.getNameOrAlias());
6✔
492

493
    if (this.required && (getValue() == null)) {
6!
494
      state.addErrorMessage("Value is required and cannot be empty.");
×
495
      return state;
×
496
    }
497
    if (this.validator != null) {
3!
498
      for (V value : this.value) {
×
499
        validator.validate(value, state);
×
500
      }
×
501
    }
502
    return state;
2✔
503
  }
504

505
  @Override
506
  public int hashCode() {
507

508
    return Objects.hash(this.name, this.value);
×
509
  }
510

511
  @Override
512
  public boolean equals(Object obj) {
513

514
    if (obj == this) {
3!
515
      return true;
2✔
516
    } else if ((obj == null) || (obj.getClass() != getClass())) {
×
517
      return false;
×
518
    }
519
    Property<?> other = (Property<?>) obj;
×
520
    if (!Objects.equals(this.name, other.name)) {
×
521
      return false;
×
522
    } else if (!Objects.equals(this.value, other.value)) {
×
523
      return false;
×
524
    }
525
    return true;
×
526
  }
527

528
  @Override
529
  public String toString() {
530

531
    StringBuilder sb = new StringBuilder();
4✔
532
    sb.append(getClass().getSimpleName());
6✔
533
    sb.append("[");
4✔
534
    if (this.name.isEmpty()) {
4✔
535
      sb.append(this.alias);
6✔
536
    } else {
537
      sb.append(this.name);
5✔
538
      if (this.alias != null) {
3✔
539
        sb.append(" | ");
4✔
540
        sb.append(this.alias);
5✔
541
      }
542
    }
543
    sb.append(":");
4✔
544
    sb.append(getValueAsString());
5✔
545
    sb.append("]");
4✔
546
    return sb.toString();
3✔
547
  }
548

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