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

devonfw / IDEasy / 12181235589

05 Dec 2024 01:58PM UTC coverage: 66.902% (-0.02%) from 66.917%
12181235589

push

github

web-flow
#508: enabled autocompletion for commandlet options (#833)

2527 of 4130 branches covered (61.19%)

Branch coverage included in aggregate %.

6577 of 9478 relevant lines covered (69.39%)

3.06 hits per line

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

73.58
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
    this.value.add(value);
5✔
246
  }
1✔
247

248
  /**
249
   * Clears the {@link #value value} list.
250
   */
251
  public void clearValue() {
252

253
    this.value.clear();
3✔
254
  }
1✔
255

256
  public void addValue(V value) {
257

258
    if (!this.multivalued) {
3!
259
      throw new IllegalStateException("not multivalued");
×
260
    }
261
    this.value.add(value);
5✔
262
  }
1✔
263

264
  /**
265
   * @param value the new {@link #getValue() value} to set.
266
   * @param i the position to set.
267
   */
268
  public void setValue(V value, int i) {
269

270
    this.value.set(i, value);
×
271
  }
×
272

273
  /**
274
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
275
   * @param context the {@link IdeContext}
276
   * @see #getValueAsString()
277
   */
278
  public void setValueAsString(String valueAsString, IdeContext context) {
279

280
    if (valueAsString == null) {
2!
281
      setValue(getNullValue());
×
282
    } else {
283
      setValue(parse(valueAsString, context));
6✔
284
    }
285
  }
1✔
286

287
  /**
288
   * Like {@link #setValueAsString(String, IdeContext)} but with exception handling.
289
   *
290
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
291
   * @param context the {@link IdeContext}
292
   * @param commandlet the {@link Commandlet} owning this property.
293
   * @return {@code true} if the value has been assigned successfully, {@code false} otherwise (an error occurred).
294
   */
295
  public final boolean assignValueAsString(String valueAsString, IdeContext context, Commandlet commandlet) {
296

297
    try {
298
      setValueAsString(valueAsString, context);
4✔
299
      return true;
2✔
300
    } catch (Exception e) {
×
301
      if (e instanceof IllegalArgumentException) {
×
302
        context.warning(INVALID_ARGUMENT, valueAsString, getNameOrAlias(), commandlet.getName());
×
303
      } else {
304
        context.warning(INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE, valueAsString, getNameOrAlias(), commandlet.getName(), e.getMessage());
×
305
      }
306
      return false;
×
307
    }
308
  }
309

310
  /**
311
   * @return the {@code null} value.
312
   */
313
  protected V getNullValue() {
314

315
    return null;
×
316
  }
317

318
  /**
319
   * @param valueAsString the value to parse given as {@link String}.
320
   * @param context the {@link IdeContext}.
321
   * @return the parsed value.
322
   */
323
  public abstract V parse(String valueAsString, IdeContext context);
324

325
  /**
326
   * @param args the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
327
   * @param context the {@link IdeContext}.
328
   * @param commandlet the {@link Commandlet} owning this property.
329
   * @param collector the {@link CompletionCandidateCollector}.
330
   * @return {@code true} if it matches, {@code false} otherwise.
331
   */
332
  public boolean apply(CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
333

334
    CliArgument argument = args.current();
3✔
335
    if (argument.isCompletion()) {
3✔
336
      int size = collector.getCandidates().size();
4✔
337
      complete(argument, args, context, commandlet, collector);
7✔
338
      return (collector.getCandidates().size() > size);
9✔
339
    }
340
    boolean option = isOption();
3✔
341
    if (option && !argument.isOption()) {
5✔
342
      return false;
2✔
343
    }
344
    if (!option && argument.isOption() && args.isSplitShortOpts()) {
8✔
345
      return false;
2✔
346
    }
347
    String argValue = null;
2✔
348
    boolean lookahead = false;
2✔
349
    if (this.name.isEmpty()) {
4✔
350
      argValue = argument.get();
4✔
351
    } else {
352
      if (!matches(argument.getKey())) {
5✔
353
        return false;
2✔
354
      }
355
      argValue = argument.getValue();
3✔
356
      if (argValue == null) {
2!
357
        argument = args.next();
3✔
358
        if (argument.isCompletion()) {
3✔
359
          completeValue(argument.get(), context, commandlet, collector);
7✔
360
          return true;
2✔
361
        } else {
362
          if (!argument.isEnd()) {
3✔
363
            argValue = argument.get();
3✔
364
          }
365
          lookahead = true;
2✔
366
        }
367
      }
368
    }
369
    return applyValue(argValue, lookahead, args, context, commandlet, collector);
9✔
370
  }
371

372
  /**
373
   * @param argValue the value to set as {@link String}.
374
   * @param lookahead - {@code true} if the given {@code argValue} is taken as lookahead from the next value, {@code false} otherwise.
375
   * @param args the {@link CliArguments}.
376
   * @param context the {@link IdeContext}.
377
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
378
   * @param collector the {@link CompletionCandidateCollector}.
379
   * @return {@code true} if it matches, {@code false} otherwise.
380
   */
381
  protected boolean applyValue(String argValue, boolean lookahead, CliArguments args, IdeContext context, Commandlet commandlet,
382
      CompletionCandidateCollector collector) {
383

384
    boolean success = assignValueAsString(argValue, context, commandlet);
6✔
385

386
    if (success) {
2!
387
      if (this.multivalued) {
3✔
388
        while (success && args.hasNext()) {
5!
389
          CliArgument arg = args.next();
×
390
          success = assignValueAsString(arg.get(), context, commandlet);
×
391
        }
×
392
      }
393
      args.next();
3✔
394
    }
395
    return success;
2✔
396
  }
397

398
  /**
399
   * Performs auto-completion for the {@code arg}.
400
   *
401
   * @param argument the {@link CliArgument CLI argument}.
402
   * @param args the {@link CliArguments}.
403
   * @param context the {@link IdeContext}.
404
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
405
   * @param collector the {@link CompletionCandidateCollector}.
406
   */
407
  protected void complete(CliArgument argument, CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
408

409
    String arg = argument.get();
3✔
410
    if (this.name.isEmpty()) {
4✔
411
      int count = collector.getCandidates().size();
4✔
412
      completeValue(arg, context, commandlet, collector);
6✔
413
      if (collector.getCandidates().size() > count) {
5!
414
        args.next();
3✔
415
      }
416
      return;
1✔
417
    }
418
    if (this.name.startsWith(arg)) {
5✔
419
      collector.add(this.name, null, this, commandlet);
7✔
420
    }
421
    if (this.alias != null) {
3✔
422
      if (this.alias.startsWith(arg)) {
5✔
423
        collector.add(this.alias, null, this, commandlet);
8✔
424
      } else if ((this.alias.length() == 2) && (this.alias.charAt(0) == '-') && argument.isShortOption()) {
14!
425
        char opt = this.alias.charAt(1); // e.g. arg="-do" and alias="-f" -complete-> "-dof"
5✔
426
        if (arg.indexOf(opt) < 0) {
4✔
427
          collector.add(arg + opt, null, this, commandlet);
8✔
428
        }
429
      }
430
    }
431
    String value = argument.getValue();
3✔
432
    if (value != null) {
2!
433
      String key = argument.getKey();
×
434
      if (this.name.equals(key) || Objects.equals(this.alias, key)) {
×
435
        completeValue(value, context, commandlet, new CompletionCandidateCollectorAdapter(key + "=", collector));
×
436
      }
437
    }
438
  }
1✔
439

440
  /**
441
   * Performs auto-completion for the {@code arg} as {@link #getValue() property value}.
442
   *
443
   * @param arg the {@link CliArgument#get() CLI argument}.
444
   * @param context the {@link IdeContext}.
445
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
446
   * @param collector the {@link CompletionCandidateCollector}.
447
   */
448
  protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
449

450
  }
1✔
451

452
  /**
453
   * @param nameOrAlias the potential {@link #getName() name} or {@link #getAlias() alias} to match.
454
   * @return {@code true} if the given {@code nameOrAlias} is equal to {@link #getName() name} or {@link #getAlias() alias}, {@code false} otherwise.
455
   */
456
  public boolean matches(String nameOrAlias) {
457

458
    return this.name.equals(nameOrAlias) || Objects.equals(this.alias, nameOrAlias);
14✔
459
  }
460

461
  /**
462
   * @return {@code true} if this {@link Property} is valid, {@code false} if it is {@link #isRequired() required} but no {@link #getValue() value} has been
463
   *     set.
464
   * @throws RuntimeException if the {@link #getValue() value} is violating given constraints. This is checked by the optional {@link Consumer} function
465
   *     given at construction time.
466
   */
467
  public ValidationResult validate() {
468

469
    ValidationState state = new ValidationState(this.getNameOrAlias());
6✔
470

471
    if (this.required && (getValue() == null)) {
6!
472
      state.addErrorMessage("Value is required and cannot be empty.");
×
473
      return state;
×
474
    }
475
    if (this.validator != null) {
3!
476
      for (V value : this.value) {
×
477
        validator.validate(value, state);
×
478
      }
×
479
    }
480
    return state;
2✔
481
  }
482

483
  @Override
484
  public int hashCode() {
485

486
    return Objects.hash(this.name, this.value);
×
487
  }
488

489
  @Override
490
  public boolean equals(Object obj) {
491

492
    if (obj == this) {
3!
493
      return true;
2✔
494
    } else if ((obj == null) || (obj.getClass() != getClass())) {
×
495
      return false;
×
496
    }
497
    Property<?> other = (Property<?>) obj;
×
498
    if (!Objects.equals(this.name, other.name)) {
×
499
      return false;
×
500
    } else if (!Objects.equals(this.value, other.value)) {
×
501
      return false;
×
502
    }
503
    return true;
×
504
  }
505

506
  @Override
507
  public String toString() {
508

509
    StringBuilder sb = new StringBuilder();
4✔
510
    sb.append(getClass().getSimpleName());
6✔
511
    sb.append("[");
4✔
512
    if (this.name.isEmpty()) {
4✔
513
      sb.append(this.alias);
6✔
514
    } else {
515
      sb.append(this.name);
5✔
516
      if (this.alias != null) {
3✔
517
        sb.append(" | ");
4✔
518
        sb.append(this.alias);
5✔
519
      }
520
    }
521
    sb.append(":");
4✔
522
    sb.append(getValueAsString());
5✔
523
    sb.append("]");
4✔
524
    return sb.toString();
3✔
525
  }
526

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