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

devonfw / IDEasy / 28790126080

06 Jul 2026 12:03PM UTC coverage: 71.844% (+0.01%) from 71.83%
28790126080

push

github

web-flow
#1178: Clean up and Solution for new qualifier snapshot (#2057)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

4785 of 7350 branches covered (65.1%)

Branch coverage included in aggregate %.

12257 of 16371 relevant lines covered (74.87%)

3.17 hits per line

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

81.85
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 final boolean snapshot;
37

38
  private VersionIdentifier(VersionSegment start) {
39

40
    super();
2✔
41
    boolean hasSnapshot = false;
2✔
42

43
    Objects.requireNonNull(start);
3✔
44
    this.start = start;
3✔
45
    boolean isValid = this.start.getSeparator().isEmpty() && this.start.getLettersString().isEmpty();
14✔
46
    boolean hasPositiveNumber = false;
2✔
47
    VersionLetters dev = VersionLetters.EMPTY;
2✔
48
    VersionSegment segment = this.start;
3✔
49
    while (segment != null) {
2✔
50
      if (!segment.isValid()) {
3✔
51
        isValid = false;
3✔
52
      } else if (segment.getNumber() > 0) {
3✔
53
        hasPositiveNumber = true;
2✔
54
      }
55
      VersionLetters segmentLetters = segment.getLetters();
3✔
56

57
      if (segmentLetters.isSnapshot()) {
3✔
58
        hasSnapshot = true;
2✔
59
      }
60

61
      if (segmentLetters.isDevelopmentPhase()) {
3✔
62
        if (dev.isEmpty()) {
3✔
63
          dev = segmentLetters;
3✔
64
        } else {
65
          dev = VersionLetters.UNDEFINED;
2✔
66
          isValid = false;
2✔
67
        }
68
      }
69
      segment = segment.getNextOrNull();
3✔
70
    }
1✔
71
    this.snapshot = hasSnapshot;
3✔
72
    this.developmentPhase = dev;
3✔
73
    this.valid = isValid && hasPositiveNumber;
9✔
74
  }
1✔
75

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

110
  /**
111
   * Finds the closest versions to the requested version pattern by matching the major version segment.
112
   *
113
   * @param version the requested version pattern or version.
114
   * @param versions the available versions to choose from.
115
   * @param maxCount the maximum number of versions to return.
116
   * @return a list of the closest matching versions.
117
   */
118
  private static List<VersionIdentifier> findClosestVersions(GenericVersionRange version, List<VersionIdentifier> versions, int maxCount) {
119

120
    if (version instanceof VersionIdentifier vi && !vi.isPattern()) {
9!
121
      long requestedMajor = vi.getStart().getNumber();
×
122
      List<VersionIdentifier> majorMatches = new ArrayList<>();
×
123
      for (VersionIdentifier v : versions) {
×
124
        if (v.getStart().getNumber() == requestedMajor) {
×
125
          majorMatches.add(v);
×
126
          if (majorMatches.size() >= maxCount) {
×
127
            break;
×
128
          }
129
        }
130
      }
×
131
      if (!majorMatches.isEmpty()) {
×
132
        return majorMatches;
×
133
      }
134
    }
135
    return versions.size() <= maxCount ? versions : versions.subList(0, maxCount);
7!
136
  }
137

138
  /**
139
   * @return the first {@link VersionSegment} of this {@link VersionIdentifier}. To get other segments use {@link VersionSegment#getNextOrEmpty()} or
140
   *     {@link VersionSegment#getNextOrNull()}.
141
   */
142
  public VersionSegment getStart() {
143

144
    return this.start;
3✔
145
  }
146

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

166
    return this.valid;
3✔
167
  }
168

169
  @Override
170
  public boolean isPattern() {
171

172
    VersionSegment segment = this.start;
3✔
173
    while (segment != null) {
2✔
174
      if (segment.isPattern()) {
3✔
175
        return true;
2✔
176
      }
177
      segment = segment.getNextOrNull();
4✔
178
    }
179
    return false;
2✔
180
  }
181

182

183
  /**
184
   * @return {@code true} if this is a stable version, {@code false} otherwise.
185
   * @see VersionLetters#isStable()
186
   */
187
  public boolean isStable() {
188

189
    return !this.snapshot && this.developmentPhase.isStable();
11✔
190
  }
191

192
  /**
193
   * @return the {@link VersionLetters#isDevelopmentPhase() development phase} of this {@link VersionIdentifier}. Will be {@link VersionLetters#EMPTY} if no
194
   *     development phase is specified in any {@link VersionSegment} and will be {@link VersionLetters#UNDEFINED} if more than one
195
   *     {@link VersionLetters#isDevelopmentPhase() development phase} is specified (e.g. "1.0-alpha1.rc2").
196
   */
197
  public VersionLetters getDevelopmentPhase() {
198

199
    return this.developmentPhase;
3✔
200
  }
201

202
  @Override
203
  public VersionComparisonResult compareVersion(VersionIdentifier other) {
204

205
    if (other == null) {
2✔
206
      return VersionComparisonResult.GREATER_UNSAFE;
2✔
207
    }
208
    VersionSegment thisSegment = this.start;
3✔
209
    VersionSegment otherSegment = other.start;
3✔
210
    VersionComparisonResult result = null;
2✔
211
    boolean unsafe = false;
2✔
212
    boolean todo = true;
2✔
213
    do {
214
      result = thisSegment.compareVersion(otherSegment);
4✔
215
      if (result.isEqual()) {
3✔
216
        if (thisSegment.isEmpty() && otherSegment.isEmpty()) {
6!
217
          todo = false;
3✔
218
        } else if (result.isUnsafe()) {
3!
219
          unsafe = true;
×
220
        }
221
      } else {
222
        todo = false;
2✔
223
      }
224
      thisSegment = thisSegment.getNextOrEmpty();
3✔
225
      otherSegment = otherSegment.getNextOrEmpty();
3✔
226
    } while (todo);
2✔
227
    if (unsafe) {
2!
228
      return result.withUnsafe();
×
229
    }
230
    return result;
2✔
231
  }
232

233
  /**
234
   * @param other the {@link VersionIdentifier} to be matched.
235
   * @return {@code true} if this {@link VersionIdentifier} is equal to the given {@link VersionIdentifier} or this {@link VersionIdentifier} is a pattern
236
   *     version (e.g. "17*" or "17.*") and the given {@link VersionIdentifier} matches to that pattern.
237
   */
238
  public boolean matches(VersionIdentifier other) {
239

240
    if (other == null) {
2✔
241
      return false;
2✔
242
    }
243
    VersionSegment thisSegment = this.start;
3✔
244
    VersionSegment otherSegment = other.start;
3✔
245
    while (true) {
246
      VersionMatchResult matchResult = thisSegment.matches(otherSegment);
4✔
247
      if (matchResult == VersionMatchResult.MATCH) {
3✔
248
        return true;
2✔
249
      } else if (matchResult == VersionMatchResult.MISMATCH) {
3✔
250
        return false;
2✔
251
      }
252
      thisSegment = thisSegment.getNextOrEmpty();
3✔
253
      otherSegment = otherSegment.getNextOrEmpty();
3✔
254
    }
1✔
255
  }
256

257
  /**
258
   * Increment the specified segment. For examples see {@code VersionIdentifierTest.testIncrement()}.
259
   *
260
   * @param digitNumber the index of the {@link VersionSegment} to increment. All segments before will remain untouched and all following segments will be
261
   *     set to zero.
262
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
263
   * @return the incremented {@link VersionIdentifier}.
264
   */
265
  public VersionIdentifier incrementSegment(int digitNumber, boolean keepLetters) {
266

267
    if (isPattern()) {
3!
268
      throw new IllegalStateException("Cannot increment version pattern: " + toString());
×
269
    }
270
    VersionSegment newStart = this.start.increment(digitNumber, keepLetters);
6✔
271
    return new VersionIdentifier(newStart);
5✔
272
  }
273

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

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

296
  /**
297
   * Increment the third digit (patch or micro version).
298
   *
299
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
300
   * @return the incremented {@link VersionIdentifier}.
301
   * @see #incrementSegment(int, boolean)
302
   */
303
  public VersionIdentifier incrementPatch(boolean keepLetters) {
304
    return incrementSegment(2, keepLetters);
5✔
305
  }
306

307
  /**
308
   * Increment the last segment.
309
   *
310
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
311
   * @return the incremented {@link VersionIdentifier}.
312
   * @see #incrementSegment(int, boolean)
313
   */
314
  public VersionIdentifier incrementLastDigit(boolean keepLetters) {
315

316
    return incrementSegment(this.start.countDigits() - 1, keepLetters);
9✔
317
  }
318

319
  @Override
320
  public VersionIdentifier getMin() {
321

322
    return this;
×
323
  }
324

325
  @Override
326
  public VersionIdentifier getMax() {
327

328
    return this;
×
329
  }
330

331
  @Override
332
  public boolean contains(VersionIdentifier version) {
333

334
    return matches(version);
4✔
335
  }
336

337
  @Override
338
  public int hashCode() {
339

340
    VersionSegment segment = this.start;
×
341
    int hash = 1;
×
342
    while (segment != null) {
×
343
      hash = hash * 31 + segment.hashCode();
×
344
      segment = segment.getNextOrNull();
×
345
    }
346
    return hash;
×
347
  }
348

349
  @Override
350
  public boolean equals(Object obj) {
351

352
    if (obj == this) {
3✔
353
      return true;
2✔
354
    } else if (!(obj instanceof VersionIdentifier)) {
3✔
355
      return false;
2✔
356
    }
357
    VersionIdentifier other = (VersionIdentifier) obj;
3✔
358
    return Objects.equals(this.start, other.start);
6✔
359
  }
360

361
  @Override
362
  @JsonSerialize
363
  public String toString() {
364

365
    StringBuilder sb = new StringBuilder();
4✔
366
    VersionSegment segment = this.start;
3✔
367
    while (segment != null) {
2✔
368
      sb.append(segment.toString());
5✔
369
      segment = segment.getNextOrNull();
4✔
370
    }
371
    return sb.toString();
3✔
372
  }
373

374
  /**
375
   * @param version the {@link #toString() string representation} of the {@link VersionIdentifier} to parse.
376
   * @return the parsed {@link VersionIdentifier}.
377
   */
378
  @JsonCreator
379
  public static VersionIdentifier of(String version) {
380

381
    if (version == null) {
2✔
382
      return null;
2✔
383
    }
384
    version = version.trim();
3✔
385
    if (version.equals("latest") || version.equals("*")) {
8!
386
      return VersionIdentifier.LATEST;
2✔
387
    }
388
    assert !version.contains(" ") && !version.contains("\n") && !version.contains("\t") : version;
13!
389
    VersionSegment startSegment = VersionSegment.of(version);
3✔
390
    if (startSegment == null) {
2✔
391
      return null;
2✔
392
    }
393
    return new VersionIdentifier(startSegment);
5✔
394
  }
395

396
  /**
397
   * @param v1 the first {@link VersionIdentifier}.
398
   * @param v2 the second {@link VersionIdentifier}.
399
   * @param treatNullAsNegativeInfinity {@code true} to treat {@code null} as negative infinity, {@code false} otherwise (positive infinity).
400
   * @return the null-safe {@link #compareVersion(VersionIdentifier) comparison} of the two {@link VersionIdentifier}s.
401
   */
402
  public static VersionComparisonResult compareVersion(VersionIdentifier v1, VersionIdentifier v2, boolean treatNullAsNegativeInfinity) {
403

404
    if (v1 == null) {
2✔
405
      if (v2 == null) {
2!
406
        return VersionComparisonResult.EQUAL;
×
407
      } else if (treatNullAsNegativeInfinity) {
2✔
408
        return VersionComparisonResult.LESS;
2✔
409
      }
410
      return VersionComparisonResult.GREATER;
2✔
411
    } else if (v2 == null) {
2✔
412
      if (treatNullAsNegativeInfinity) {
2✔
413
        return VersionComparisonResult.GREATER;
2✔
414
      }
415
      return VersionComparisonResult.LESS;
2✔
416
    }
417
    return v1.compareVersion(v2);
4✔
418
  }
419

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