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

devonfw / IDEasy / 22345446363

24 Feb 2026 09:49AM UTC coverage: 70.247% (-0.2%) from 70.474%
22345446363

Pull #1714

github

web-flow
Merge 5655b6589 into 379acdc9d
Pull Request #1714: #404: #1713: advanced logging

4065 of 6384 branches covered (63.67%)

Branch coverage included in aggregate %.

10597 of 14488 relevant lines covered (73.14%)

3.08 hits per line

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

68.11
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
  /**
57
   * The constructor.
58
   *
59
   * @param name the {@link #getName() property name}.
60
   * @param required the {@link #isRequired() required flag}.
61
   * @param alias the {@link #getAlias() property alias}.
62
   */
63
  public Property(String name, boolean required, String alias) {
64

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

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

84
    super();
2✔
85
    this.name = name;
3✔
86
    this.required = required;
3✔
87
    this.alias = alias;
3✔
88
    this.validator = validator;
3✔
89
    this.multivalued = multivalued;
3✔
90
  }
1✔
91

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

97
    return this.name;
3✔
98
  }
99

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

106
    return this.alias;
3✔
107
  }
108

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

114
    if (this.name.isEmpty()) {
4✔
115
      return this.alias;
3✔
116
    }
117
    return this.name;
3✔
118
  }
119

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

126
    return this.required;
3✔
127
  }
128

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

134
    return "".equals(this.name);
×
135
  }
136

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

145
    return this.name.startsWith("-");
5✔
146
  }
147

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

154
    return isMultiValued();
×
155
  }
156

157
  /**
158
   * 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}
159
   * of a {@link Commandlet}.
160
   *
161
   * @return {@code true} if multi-valued, {@code false} otherwise.
162
   */
163
  public boolean isMultiValued() {
164

165
    return this.multivalued;
3✔
166
  }
167

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

177
    return !isOption();
7✔
178
  }
179

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

185
  /**
186
   * @return the value of this property.
187
   * @see #setValue(Object)
188
   */
189
  public V getValue() {
190

191
    if (this.value.isEmpty()) {
4✔
192
      return null;
2✔
193
    } else {
194
      return this.value.getFirst();
4✔
195
    }
196
  }
197

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

204
    return this.value.get(i);
5✔
205
  }
206

207
  /**
208
   * @return amount of values.
209
   */
210
  public int getValueCount() {
211

212
    return this.value.size();
4✔
213
  }
214

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

221
    if (getValue() == null) {
×
222
      return null;
×
223
    }
224
    return format(getValue());
×
225
  }
226

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

232
    return new ArrayList<>(this.value);
6✔
233
  }
234

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

241
    return valueToFormat.toString();
×
242
  }
243

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

250
    if (!this.multivalued) {
3✔
251
      this.value.clear();
3✔
252
    }
253
    this.value.add(value);
5✔
254
  }
1✔
255

256
  /**
257
   * Clears the {@link #value value} list.
258
   */
259
  public void clearValue() {
260

261
    this.value.clear();
3✔
262
  }
1✔
263

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

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

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

282
    this.value.set(i, value);
6✔
283
  }
1✔
284

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

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

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

309
    try {
310
      setValueAsString(valueAsString, context);
4✔
311
      return true;
2✔
312
    } catch (Exception e) {
×
313
      if (e instanceof IllegalArgumentException) {
×
314
        LOG.warn(INVALID_ARGUMENT, valueAsString, getNameOrAlias(), commandlet.getName());
×
315
      } else {
316
        LOG.warn(INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE, valueAsString, getNameOrAlias(), commandlet.getName(), e.getMessage());
×
317
      }
318
      return false;
×
319
    }
320
  }
321

322
  /**
323
   * @return the {@code null} value.
324
   */
325
  protected V getNullValue() {
326

327
    return null;
×
328
  }
329

330
  /**
331
   * @param valueAsString the value to parse given as {@link String}.
332
   * @param context the {@link IdeContext}.
333
   * @return the parsed value.
334
   */
335
  public abstract V parse(String valueAsString, IdeContext context);
336

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

346
    return apply(this.name, args, context, commandlet, collector);
9✔
347
  }
348

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

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

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

409
    boolean success = assignValueAsString(argValue, context, commandlet);
6✔
410

411
    if (success) {
2!
412
      if (this.multivalued) {
3✔
413
        while (success && args.hasNext()) {
5!
414
          CliArgument arg = args.next();
×
415
          success = assignValueAsString(arg.get(), context, commandlet);
×
416
        }
×
417
      }
418
    }
419
    args.next();
3✔
420
    return success;
2✔
421
  }
422

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

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

467
  /**
468
   * Performs auto-completion for the {@code arg} as {@link #getValue() property value}.
469
   *
470
   * @param arg the {@link CliArgument#get() CLI argument}.
471
   * @param context the {@link IdeContext}.
472
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
473
   * @param collector the {@link CompletionCandidateCollector}.
474
   */
475
  protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
476

477
  }
1✔
478

479
  /**
480
   * @param nameOrAlias the potential {@link #getName() name} or {@link #getAlias() alias} to match.
481
   * @return {@code true} if the given {@code nameOrAlias} is equal to {@link #getName() name} or {@link #getAlias() alias}, {@code false} otherwise.
482
   */
483
  public boolean matches(String nameOrAlias) {
484

485
    return this.name.equals(nameOrAlias) || Objects.equals(this.alias, nameOrAlias);
14✔
486
  }
487

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

496
    ValidationState state = new ValidationState(this.getNameOrAlias());
6✔
497

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

510
  @Override
511
  public int hashCode() {
512

513
    return Objects.hash(this.name, this.value);
×
514
  }
515

516
  @Override
517
  public boolean equals(Object obj) {
518

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

533
  @Override
534
  public String toString() {
535

536
    StringBuilder sb = new StringBuilder();
×
537
    sb.append(getClass().getSimpleName());
×
538
    sb.append("[");
×
539
    if (this.name.isEmpty()) {
×
540
      sb.append(this.alias);
×
541
    } else {
542
      sb.append(this.name);
×
543
      if (this.alias != null) {
×
544
        sb.append(" | ");
×
545
        sb.append(this.alias);
×
546
      }
547
    }
548
    sb.append(":");
×
549
    sb.append(getValueAsString());
×
550
    sb.append("]");
×
551
    return sb.toString();
×
552
  }
553

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