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

devonfw / IDEasy / 25505184832

07 May 2026 03:23PM UTC coverage: 70.917% (+0.2%) from 70.741%
25505184832

Pull #1858

github

web-flow
Merge f96fc2947 into fd215c395
Pull Request #1858: #1457: Improve CLI error messages with suggestions

4523 of 7036 branches covered (64.28%)

Branch coverage included in aggregate %.

11527 of 15596 relevant lines covered (73.91%)

3.13 hits per line

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

81.12
cli/src/main/java/com/devonfw/tools/ide/version/VersionIdentifier.java
1
package com.devonfw.tools.ide.version;
2

3
import java.util.ArrayList;
4
import java.util.List;
5
import java.util.Objects;
6
import java.util.stream.Collectors;
7

8
import org.slf4j.Logger;
9
import org.slf4j.LoggerFactory;
10

11
import com.devonfw.tools.ide.cli.CliException;
12
import com.devonfw.tools.ide.tool.ToolCommandlet;
13
import com.fasterxml.jackson.annotation.JsonCreator;
14
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
15

16
/**
17
 * Data-type to represent a {@link VersionIdentifier} in a structured way and allowing {@link #compareVersion(VersionIdentifier) comparison} of
18
 * {@link VersionIdentifier}s.
19
 */
20
public final class VersionIdentifier implements VersionObject<VersionIdentifier>, GenericVersionRange {
21

22
  private static final Logger LOG = LoggerFactory.getLogger(VersionIdentifier.class);
3✔
23

24
  /** {@link VersionIdentifier} "*" that will resolve to the latest stable version. */
25
  public static final VersionIdentifier LATEST = new VersionIdentifier(VersionSegment.of("*"));
6✔
26

27
  /** {@link VersionIdentifier} "*!" that will resolve to the latest snapshot. */
28
  public static final VersionIdentifier LATEST_UNSTABLE = VersionIdentifier.of("*!");
4✔
29

30
  private final VersionSegment start;
31

32
  private final VersionLetters developmentPhase;
33

34
  private final boolean valid;
35

36
  private VersionIdentifier(VersionSegment start) {
37

38
    super();
2✔
39
    Objects.requireNonNull(start);
3✔
40
    this.start = start;
3✔
41
    boolean isValid = this.start.getSeparator().isEmpty() && this.start.getLettersString().isEmpty();
14✔
42
    boolean hasPositiveNumber = false;
2✔
43
    VersionLetters dev = VersionLetters.EMPTY;
2✔
44
    VersionSegment segment = this.start;
3✔
45
    while (segment != null) {
2✔
46
      if (!segment.isValid()) {
3✔
47
        isValid = false;
3✔
48
      } else if (segment.getNumber() > 0) {
3✔
49
        hasPositiveNumber = true;
2✔
50
      }
51
      VersionLetters segmentLetters = segment.getLetters();
3✔
52
      if (segmentLetters.isDevelopmentPhase()) {
3✔
53
        if (dev.isEmpty()) {
3✔
54
          dev = segmentLetters;
3✔
55
        } else {
56
          dev = VersionLetters.UNDEFINED;
2✔
57
          isValid = false;
2✔
58
        }
59
      }
60
      segment = segment.getNextOrNull();
3✔
61
    }
1✔
62
    this.developmentPhase = dev;
3✔
63
    this.valid = isValid && hasPositiveNumber;
9✔
64
  }
1✔
65

66
  /**
67
   * Resolves a version pattern against a list of available versions.
68
   *
69
   * @param version the version pattern to resolve
70
   * @param versions the
71
   *     {@link com.devonfw.tools.ide.tool.repository.ToolRepository#getSortedVersions(String, String, ToolCommandlet) available versions, sorted in descending
72
   *     order}.
73
   * @return the resolved version
74
   */
75
  public static VersionIdentifier resolveVersionPattern(GenericVersionRange version, List<VersionIdentifier> versions) {
76
    if (version == null) {
2!
77
      version = LATEST;
×
78
    }
79
    if (!version.isPattern()) {
3✔
80
      for (VersionIdentifier vi : versions) {
10!
81
        if (vi.equals(version)) {
4✔
82
          LOG.debug("Resolved version {} to version {}", version, vi);
5✔
83
          return vi;
2✔
84
        }
85
      }
1✔
86
    }
87
    for (VersionIdentifier vi : versions) {
10✔
88
      if (version.contains(vi)) {
4✔
89
        LOG.debug("Resolved version pattern {} to version {}", version, vi);
5✔
90
        return vi;
2✔
91
      }
92
    }
1✔
93
    List<VersionIdentifier> closest = findClosestVersions(version, versions, 5);
5✔
94
    String closestStr = closest.stream().map(Object::toString).collect(Collectors.joining(", "));
9✔
95
    throw new CliException(
5✔
96
        "Could not find any version matching '" + version + "' - there are " + versions.size()
5✔
97
            + " version(s) available but none matched!\nDid you mean one of: " + closestStr + "?");
98
  }
99

100
  /**
101
   * Finds the closest versions to the requested version pattern by matching the major version segment.
102
   *
103
   * @param version the requested version pattern or version.
104
   * @param versions the available versions to choose from.
105
   * @param maxCount the maximum number of versions to return.
106
   * @return a list of the closest matching versions.
107
   */
108
  private static List<VersionIdentifier> findClosestVersions(GenericVersionRange version, List<VersionIdentifier> versions, int maxCount) {
109

110
    if (version instanceof VersionIdentifier vi && !vi.isPattern()) {
9!
111
      long requestedMajor = vi.getStart().getNumber();
×
112
      List<VersionIdentifier> majorMatches = new ArrayList<>();
×
113
      for (VersionIdentifier v : versions) {
×
114
        if (v.getStart().getNumber() == requestedMajor) {
×
115
          majorMatches.add(v);
×
116
          if (majorMatches.size() >= maxCount) {
×
117
            break;
×
118
          }
119
        }
120
      }
×
121
      if (!majorMatches.isEmpty()) {
×
122
        return majorMatches;
×
123
      }
124
    }
125
    return versions.size() <= maxCount ? versions : versions.subList(0, maxCount);
7!
126
  }
127

128
  /**
129
   * @return the first {@link VersionSegment} of this {@link VersionIdentifier}. To get other segments use {@link VersionSegment#getNextOrEmpty()} or
130
   *     {@link VersionSegment#getNextOrNull()}.
131
   */
132
  public VersionSegment getStart() {
133

134
    return this.start;
3✔
135
  }
136

137
  /**
138
   * A valid {@link VersionIdentifier} has to meet the following requirements:
139
   * <ul>
140
   * <li>All {@link VersionSegment segments} themselves are {@link VersionSegment#isValid() valid}.</li>
141
   * <li>The {@link #getStart() start} {@link VersionSegment segment} shall have an {@link String#isEmpty() empty}
142
   * {@link VersionSegment#getSeparator() separator} (e.g. ".1.0" or "-1-2" are not considered valid).</li>
143
   * <li>The {@link #getStart() start} {@link VersionSegment segment} shall have an {@link String#isEmpty() empty}
144
   * {@link VersionSegment#getLettersString() letter-sequence} (e.g. "RC1" or "beta" are not considered valid).</li>
145
   * <li>Have at least one {@link VersionSegment segment} with a positive {@link VersionSegment#getNumber() number}
146
   * (e.g. "0.0.0" or "0.alpha" are not considered valid).</li>
147
   * <li>Have at max one {@link VersionSegment segment} with a {@link VersionSegment#getPhase() phase} that is a real
148
   * {@link VersionPhase#isDevelopmentPhase() development phase} (e.g. "1.alpha1.beta2" or "1.0.rc1-milestone2" are not
149
   * considered valid).</li>
150
   * <li>It is NOT a {@link #isPattern() pattern}.</li>
151
   * </ul>
152
   */
153
  @Override
154
  public boolean isValid() {
155

156
    return this.valid;
3✔
157
  }
158

159
  @Override
160
  public boolean isPattern() {
161

162
    VersionSegment segment = this.start;
3✔
163
    while (segment != null) {
2✔
164
      if (segment.isPattern()) {
3✔
165
        return true;
2✔
166
      }
167
      segment = segment.getNextOrNull();
4✔
168
    }
169
    return false;
2✔
170
  }
171

172

173
  /**
174
   * @return {@code true} if this is a stable version, {@code false} otherwise.
175
   * @see VersionLetters#isStable()
176
   */
177
  public boolean isStable() {
178

179
    return this.developmentPhase.isStable();
4✔
180
  }
181

182
  /**
183
   * @return the {@link VersionLetters#isDevelopmentPhase() development phase} of this {@link VersionIdentifier}. Will be {@link VersionLetters#EMPTY} if no
184
   *     development phase is specified in any {@link VersionSegment} and will be {@link VersionLetters#UNDEFINED} if more than one
185
   *     {@link VersionLetters#isDevelopmentPhase() development phase} is specified (e.g. "1.0-alpha1.rc2").
186
   */
187
  public VersionLetters getDevelopmentPhase() {
188

189
    return this.developmentPhase;
3✔
190
  }
191

192
  @Override
193
  public VersionComparisonResult compareVersion(VersionIdentifier other) {
194

195
    if (other == null) {
2✔
196
      return VersionComparisonResult.GREATER_UNSAFE;
2✔
197
    }
198
    VersionSegment thisSegment = this.start;
3✔
199
    VersionSegment otherSegment = other.start;
3✔
200
    VersionComparisonResult result = null;
2✔
201
    boolean unsafe = false;
2✔
202
    boolean todo = true;
2✔
203
    do {
204
      result = thisSegment.compareVersion(otherSegment);
4✔
205
      if (result.isEqual()) {
3✔
206
        if (thisSegment.isEmpty() && otherSegment.isEmpty()) {
6!
207
          todo = false;
3✔
208
        } else if (result.isUnsafe()) {
3!
209
          unsafe = true;
×
210
        }
211
      } else {
212
        todo = false;
2✔
213
      }
214
      thisSegment = thisSegment.getNextOrEmpty();
3✔
215
      otherSegment = otherSegment.getNextOrEmpty();
3✔
216
    } while (todo);
2✔
217
    if (unsafe) {
2!
218
      return result.withUnsafe();
×
219
    }
220
    return result;
2✔
221
  }
222

223
  /**
224
   * @param other the {@link VersionIdentifier} to be matched.
225
   * @return {@code true} if this {@link VersionIdentifier} is equal to the given {@link VersionIdentifier} or this {@link VersionIdentifier} is a pattern
226
   *     version (e.g. "17*" or "17.*") and the given {@link VersionIdentifier} matches to that pattern.
227
   */
228
  public boolean matches(VersionIdentifier other) {
229

230
    if (other == null) {
2✔
231
      return false;
2✔
232
    }
233
    VersionSegment thisSegment = this.start;
3✔
234
    VersionSegment otherSegment = other.start;
3✔
235
    while (true) {
236
      VersionMatchResult matchResult = thisSegment.matches(otherSegment);
4✔
237
      if (matchResult == VersionMatchResult.MATCH) {
3✔
238
        return true;
2✔
239
      } else if (matchResult == VersionMatchResult.MISMATCH) {
3✔
240
        return false;
2✔
241
      }
242
      thisSegment = thisSegment.getNextOrEmpty();
3✔
243
      otherSegment = otherSegment.getNextOrEmpty();
3✔
244
    }
1✔
245
  }
246

247
  /**
248
   * Increment the specified segment. For examples see {@code VersionIdentifierTest.testIncrement()}.
249
   *
250
   * @param digitNumber the index of the {@link VersionSegment} to increment. All segments before will remain untouched and all following segments will be
251
   *     set to zero.
252
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
253
   * @return the incremented {@link VersionIdentifier}.
254
   */
255
  public VersionIdentifier incrementSegment(int digitNumber, boolean keepLetters) {
256

257
    if (isPattern()) {
3!
258
      throw new IllegalStateException("Cannot increment version pattern: " + toString());
×
259
    }
260
    VersionSegment newStart = this.start.increment(digitNumber, keepLetters);
6✔
261
    return new VersionIdentifier(newStart);
5✔
262
  }
263

264
  /**
265
   * Increment the first digit (major version).
266
   *
267
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
268
   * @return the incremented {@link VersionIdentifier}.
269
   * @see #incrementSegment(int, boolean)
270
   */
271
  public VersionIdentifier incrementMajor(boolean keepLetters) {
272
    return incrementSegment(0, keepLetters);
5✔
273
  }
274

275
  /**
276
   * Increment the second digit (minor version).
277
   *
278
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
279
   * @return the incremented {@link VersionIdentifier}.
280
   * @see #incrementSegment(int, boolean)
281
   */
282
  public VersionIdentifier incrementMinor(boolean keepLetters) {
283
    return incrementSegment(1, keepLetters);
5✔
284
  }
285

286
  /**
287
   * Increment the third digit (patch or micro version).
288
   *
289
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
290
   * @return the incremented {@link VersionIdentifier}.
291
   * @see #incrementSegment(int, boolean)
292
   */
293
  public VersionIdentifier incrementPatch(boolean keepLetters) {
294
    return incrementSegment(2, keepLetters);
5✔
295
  }
296

297
  /**
298
   * Increment the last segment.
299
   *
300
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
301
   * @return the incremented {@link VersionIdentifier}.
302
   * @see #incrementSegment(int, boolean)
303
   */
304
  public VersionIdentifier incrementLastDigit(boolean keepLetters) {
305

306
    return incrementSegment(this.start.countDigits() - 1, keepLetters);
9✔
307
  }
308

309
  @Override
310
  public VersionIdentifier getMin() {
311

312
    return this;
×
313
  }
314

315
  @Override
316
  public VersionIdentifier getMax() {
317

318
    return this;
×
319
  }
320

321
  @Override
322
  public boolean contains(VersionIdentifier version) {
323

324
    return matches(version);
4✔
325
  }
326

327
  @Override
328
  public int hashCode() {
329

330
    VersionSegment segment = this.start;
×
331
    int hash = 1;
×
332
    while (segment != null) {
×
333
      hash = hash * 31 + segment.hashCode();
×
334
      segment = segment.getNextOrNull();
×
335
    }
336
    return hash;
×
337
  }
338

339
  @Override
340
  public boolean equals(Object obj) {
341

342
    if (obj == this) {
3✔
343
      return true;
2✔
344
    } else if (!(obj instanceof VersionIdentifier)) {
3✔
345
      return false;
2✔
346
    }
347
    VersionIdentifier other = (VersionIdentifier) obj;
3✔
348
    return Objects.equals(this.start, other.start);
6✔
349
  }
350

351
  @Override
352
  @JsonSerialize
353
  public String toString() {
354

355
    StringBuilder sb = new StringBuilder();
4✔
356
    VersionSegment segment = this.start;
3✔
357
    while (segment != null) {
2✔
358
      sb.append(segment.toString());
5✔
359
      segment = segment.getNextOrNull();
4✔
360
    }
361
    return sb.toString();
3✔
362
  }
363

364
  /**
365
   * @param version the {@link #toString() string representation} of the {@link VersionIdentifier} to parse.
366
   * @return the parsed {@link VersionIdentifier}.
367
   */
368
  @JsonCreator
369
  public static VersionIdentifier of(String version) {
370

371
    if (version == null) {
2✔
372
      return null;
2✔
373
    }
374
    version = version.trim();
3✔
375
    if (version.equals("latest") || version.equals("*")) {
8!
376
      return VersionIdentifier.LATEST;
2✔
377
    }
378
    assert !version.contains(" ") && !version.contains("\n") && !version.contains("\t") : version;
13!
379
    VersionSegment startSegment = VersionSegment.of(version);
3✔
380
    if (startSegment == null) {
2✔
381
      return null;
2✔
382
    }
383
    return new VersionIdentifier(startSegment);
5✔
384
  }
385

386
  /**
387
   * @param v1 the first {@link VersionIdentifier}.
388
   * @param v2 the second {@link VersionIdentifier}.
389
   * @param treatNullAsNegativeInfinity {@code true} to treat {@code null} as negative infinity, {@code false} otherwise (positive infinity).
390
   * @return the null-safe {@link #compareVersion(VersionIdentifier) comparison} of the two {@link VersionIdentifier}s.
391
   */
392
  public static VersionComparisonResult compareVersion(VersionIdentifier v1, VersionIdentifier v2, boolean treatNullAsNegativeInfinity) {
393

394
    if (v1 == null) {
2✔
395
      if (v2 == null) {
2!
396
        return VersionComparisonResult.EQUAL;
×
397
      } else if (treatNullAsNegativeInfinity) {
2✔
398
        return VersionComparisonResult.LESS;
2✔
399
      }
400
      return VersionComparisonResult.GREATER;
2✔
401
    } else if (v2 == null) {
2✔
402
      if (treatNullAsNegativeInfinity) {
2✔
403
        return VersionComparisonResult.GREATER;
2✔
404
      }
405
      return VersionComparisonResult.LESS;
2✔
406
    }
407
    return v1.compareVersion(v2);
4✔
408
  }
409

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