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

devonfw / IDEasy / 9904178931

12 Jul 2024 07:32AM UTC coverage: 61.387% (-0.5%) from 61.842%
9904178931

push

github

web-flow
#400: fix error handling for undefined IDE_ROOT and IDE_HOME (#476)

1997 of 3575 branches covered (55.86%)

Branch coverage included in aggregate %.

5297 of 8307 relevant lines covered (63.77%)

2.8 hits per line

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

69.92
cli/src/main/java/com/devonfw/tools/ide/property/Property.java
1
package com.devonfw.tools.ide.property;
2

3
import com.devonfw.tools.ide.cli.CliArgument;
4
import com.devonfw.tools.ide.cli.CliArguments;
5
import com.devonfw.tools.ide.commandlet.Commandlet;
6
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
7
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorAdapter;
8
import com.devonfw.tools.ide.context.IdeContext;
9

10
import java.util.ArrayList;
11
import java.util.List;
12
import java.util.Objects;
13
import java.util.function.Consumer;
14

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

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

29
  private static final String INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE = INVALID_ARGUMENT + ": {}";
30

31
  /** @see #getName() */
32
  protected final String name;
33

34
  /** @see #getAlias() */
35
  protected final String alias;
36

37
  /** @see #isRequired() */
38
  protected final boolean required;
39

40
  private final Consumer<V> validator;
41

42
  /** @see #isMultiValued() */
43
  private final boolean multivalued;
44

45
  /** @see #getValue() */
46
  protected final List<V> value = new ArrayList<>();
10✔
47

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

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

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

76
    super();
2✔
77
    this.name = name;
3✔
78
    this.required = required;
3✔
79
    this.alias = alias;
3✔
80
    this.validator = validator;
3✔
81
    this.multivalued = multivalued;
3✔
82
  }
1✔
83

84
  /**
85
   * @return the name of this property.
86
   */
87
  public String getName() {
88

89
    return this.name;
3✔
90
  }
91

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

98
    return this.alias;
3✔
99
  }
100

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

106
    if (this.name.isEmpty()) {
4✔
107
      return this.alias;
3✔
108
    }
109
    return this.name;
3✔
110
  }
111

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

118
    return this.required;
3✔
119
  }
120

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

126
    return "".equals(this.name);
×
127
  }
128

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

137
    return this.name.startsWith("-");
5✔
138
  }
139

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

146
    return isMultiValued();
×
147
  }
148

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

157
    return multivalued;
3✔
158
  }
159

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

169
    return !isOption();
7✔
170
  }
171

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

177
  /**
178
   * @return the value of this property.
179
   * @see #setValue(Object)
180
   */
181
  public V getValue() {
182

183
    if (this.value.isEmpty()) {
4✔
184
      return null;
2✔
185
    } else {
186
      return this.value.get(0);
5✔
187
    }
188
  }
189

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

196
    return this.value.get(i);
5✔
197
  }
198

199
  /**
200
   * @return amount of values.
201
   */
202
  public int getValueCount() {
203

204
    return this.value.size();
4✔
205
  }
206

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

213
    if (getValue() == null) {
3✔
214
      return null;
2✔
215
    }
216
    return format(getValue());
5✔
217
  }
218

219
  /**
220
   * @param valueToFormat the value to format.
221
   * @return the given {@code value} formatted as {@link String}.
222
   */
223
  protected String format(V valueToFormat) {
224

225
    return valueToFormat.toString();
3✔
226
  }
227

228
  /**
229
   * @param value the new {@link #getValue() value} to set.
230
   * @see #getValue()
231
   */
232
  public void setValue(V value) {
233

234
    this.value.add(value);
5✔
235
  }
1✔
236

237
  /**
238
   * Clears the {@link #value value} list.
239
   */
240
  public void clearValue() {
241

242
    this.value.clear();
×
243
  }
×
244

245
  public void addValue(V value) {
246

247
    if (!this.multivalued) {
3!
248
      throw new IllegalStateException("not multivalued");
×
249
    }
250
    this.value.add(value);
5✔
251
  }
1✔
252

253
  /**
254
   * @param value the new {@link #getValue() value} to set.
255
   * @param i     the position to set.
256
   */
257
  public void setValue(V value, int i) {
258

259
    this.value.set(i, value);
×
260
  }
×
261

262
  /**
263
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
264
   * @param context       the {@link IdeContext}
265
   * @see #getValueAsString()
266
   */
267
  public void setValueAsString(String valueAsString, IdeContext context) {
268

269
    if (valueAsString == null) {
2!
270
      setValue(getNullValue());
×
271
    } else {
272
      setValue(parse(valueAsString, context));
6✔
273
    }
274
  }
1✔
275

276
  /**
277
   * Like {@link #setValueAsString(String, IdeContext)} but with exception handling.
278
   *
279
   * @param valueAsString the new {@link #getValue() value} as {@link String}.
280
   * @param context       the {@link IdeContext}
281
   * @param commandlet    the {@link Commandlet} owning this property.
282
   * @return {@code true} if the value has been assigned successfully, {@code false} otherwise (an error occurred).
283
   */
284
  public final boolean assignValueAsString(String valueAsString, IdeContext context, Commandlet commandlet) {
285

286
    try {
287
      setValueAsString(valueAsString, context);
4✔
288
      return true;
2✔
289
    } catch (Exception e) {
1✔
290
      if (e instanceof IllegalArgumentException) {
3!
291
        context.warning(INVALID_ARGUMENT, valueAsString, getNameOrAlias(), commandlet.getName());
20✔
292
      } else {
293
        context.warning(INVALID_ARGUMENT_WITH_EXCEPTION_MESSAGE, valueAsString, getNameOrAlias(), commandlet.getName(), e.getMessage());
×
294
      }
295
      return false;
2✔
296
    }
297
  }
298

299
  /**
300
   * @return the {@code null} value.
301
   */
302
  protected V getNullValue() {
303

304
    return null;
×
305
  }
306

307
  /**
308
   * @param valueAsString the value to parse given as {@link String}.
309
   * @param context       the {@link IdeContext}.
310
   * @return the parsed value.
311
   */
312
  public abstract V parse(String valueAsString, IdeContext context);
313

314
  /**
315
   * @param args       the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
316
   * @param context    the {@link IdeContext}.
317
   * @param commandlet the {@link Commandlet} owning this property.
318
   * @param collector  the {@link CompletionCandidateCollector}.
319
   * @return {@code true} if it matches, {@code false} otherwise.
320
   */
321
  public boolean apply(CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
322

323
    CliArgument argument = args.current();
3✔
324
    if (argument.isCompletion()) {
3✔
325
      int size = collector.getCandidates().size();
4✔
326
      complete(argument, args, context, commandlet, collector);
7✔
327
      if (collector.getCandidates().size() > size) { // completions added so complete matched?
5✔
328
        return true;
2✔
329
      }
330
    }
331
    boolean option = isOption();
3✔
332
    if (option && !argument.isOption()) {
5✔
333
      return false;
2✔
334
    }
335
    String argValue = null;
2✔
336
    boolean lookahead = false;
2✔
337
    if (this.name.isEmpty()) {
4✔
338
      argValue = argument.get();
4✔
339
    } else {
340
      if (!matches(argument.getKey())) {
5✔
341
        return false;
2✔
342
      }
343
      argValue = argument.getValue();
3✔
344
      if (argValue == null) {
2!
345
        argument = args.next();
3✔
346
        if (argument.isCompletion()) {
3✔
347
          completeValue(argument.get(), context, commandlet, collector);
7✔
348
          return true;
2✔
349
        } else {
350
          if (!argument.isEnd()) {
3!
351
            argValue = argument.get();
3✔
352
          }
353
          lookahead = true;
2✔
354
        }
355
      }
356
    }
357
    return applyValue(argValue, lookahead, args, context, commandlet, collector);
9✔
358
  }
359

360
  /**
361
   * @param argValue   the value to set as {@link String}.
362
   * @param lookahead  - {@code true} if the given {@code argValue} is taken as lookahead from the next value, {@code false} otherwise.
363
   * @param args       the {@link CliArguments}.
364
   * @param context    the {@link IdeContext}.
365
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
366
   * @param collector  the {@link CompletionCandidateCollector}.
367
   * @return {@code true} if it matches, {@code false} otherwise.
368
   */
369
  protected boolean applyValue(String argValue, boolean lookahead, CliArguments args, IdeContext context, Commandlet commandlet,
370
                               CompletionCandidateCollector collector) {
371

372
    boolean success = assignValueAsString(argValue, context, commandlet);
6✔
373

374
    if (success) {
2✔
375
      if (multivalued) {
3!
376

377
        while (success && args.hasNext()) {
×
378
          CliArgument arg = args.next();
×
379
          success = assignValueAsString(arg.get(), context, commandlet);
×
380
        }
×
381
      }
382
      args.next();
3✔
383
    }
384
    return success;
2✔
385
  }
386

387
  /**
388
   * Performs auto-completion for the {@code arg}.
389
   *
390
   * @param argument   the {@link CliArgument CLI argument}.
391
   * @param args       the {@link CliArguments}.
392
   * @param context    the {@link IdeContext}.
393
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
394
   * @param collector  the {@link CompletionCandidateCollector}.
395
   */
396
  protected void complete(CliArgument argument, CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
397

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

429
  /**
430
   * Performs auto-completion for the {@code arg} as {@link #getValue() property value}.
431
   *
432
   * @param arg        the {@link CliArgument#get() CLI argument}.
433
   * @param context    the {@link IdeContext}.
434
   * @param commandlet the {@link Commandlet} owning this {@link Property}.
435
   * @param collector  the {@link CompletionCandidateCollector}.
436
   */
437
  protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
438

439
  }
1✔
440

441
  /**
442
   * @param nameOrAlias the potential {@link #getName() name} or {@link #getAlias() alias} to match.
443
   * @return {@code true} if the given {@code nameOrAlias} is equal to {@link #getName() name} or {@link #getAlias() alias}, {@code false} otherwise.
444
   */
445
  public boolean matches(String nameOrAlias) {
446

447
    return this.name.equals(nameOrAlias) || Objects.equals(this.alias, nameOrAlias);
14!
448
  }
449

450
  /**
451
   * @return {@code true} if this {@link Property} is valid, {@code false} if it is {@link #isRequired() required} but no {@link #getValue() value} has been
452
   * set.
453
   * @throws RuntimeException if the {@link #getValue() value} is violating given constraints. This is checked by the optional {@link Consumer} function given
454
   *                          at construction time.
455
   */
456
  public boolean validate() {
457

458
    if (this.required && (getValue() == null)) {
×
459
      return false;
×
460
    }
461
    if (this.validator != null) {
×
462
      for (V value : this.value) {
×
463
        this.validator.accept(value);
×
464
      }
×
465
    }
466
    return true;
×
467
  }
468

469
  @Override
470
  public int hashCode() {
471

472
    return Objects.hash(this.name, this.value);
×
473
  }
474

475
  @Override
476
  public boolean equals(Object obj) {
477

478
    if (obj == this) {
×
479
      return true;
×
480
    } else if ((obj == null) || (obj.getClass() != getClass())) {
×
481
      return false;
×
482
    }
483
    Property<?> other = (Property<?>) obj;
×
484
    if (!Objects.equals(this.name, other.name)) {
×
485
      return false;
×
486
    } else if (!Objects.equals(this.value, other.value)) {
×
487
      return false;
×
488
    }
489
    return true;
×
490
  }
491

492
  @Override
493
  public String toString() {
494

495
    StringBuilder sb = new StringBuilder();
4✔
496
    sb.append(getClass().getSimpleName());
6✔
497
    sb.append("[");
4✔
498
    if (this.name.isEmpty()) {
4✔
499
      sb.append(this.alias);
6✔
500
    } else {
501
      sb.append(this.name);
5✔
502
      if (this.alias != null) {
3✔
503
        sb.append(" | ");
4✔
504
        sb.append(this.alias);
5✔
505
      }
506
    }
507
    sb.append(":");
4✔
508
    sb.append(getValueAsString());
5✔
509
    sb.append("]");
4✔
510
    return sb.toString();
3✔
511
  }
512

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