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

devonfw / IDEasy / 19868191669

02 Dec 2025 05:46PM UTC coverage: 69.852% (+0.07%) from 69.787%
19868191669

push

github

web-flow
#1633: fix CVE check to only suggest unstable versions if the requested version was unstable (#1635)

3835 of 6021 branches covered (63.69%)

Branch coverage included in aggregate %.

9812 of 13516 relevant lines covered (72.6%)

3.16 hits per line

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

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

3
import java.util.List;
4
import java.util.Objects;
5

6
import com.devonfw.tools.ide.cli.CliException;
7
import com.devonfw.tools.ide.log.IdeLogger;
8
import com.devonfw.tools.ide.tool.ToolCommandlet;
9
import com.fasterxml.jackson.annotation.JsonCreator;
10
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
11

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

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

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

24
  private final VersionSegment start;
25

26
  private final VersionLetters developmentPhase;
27

28
  private final boolean valid;
29

30
  private VersionIdentifier(VersionSegment start) {
31

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

60
  /**
61
   * Resolves a version pattern against a list of available versions.
62
   *
63
   * @param version the version pattern to resolve
64
   * @param versions the
65
   *     {@link com.devonfw.tools.ide.tool.repository.ToolRepository#getSortedVersions(String, String, ToolCommandlet) available versions, sorted in descending
66
   *     order}.
67
   * @param logger the {@link IdeLogger}.
68
   * @return the resolved version
69
   */
70
  public static VersionIdentifier resolveVersionPattern(GenericVersionRange version, List<VersionIdentifier> versions, IdeLogger logger) {
71
    if (version == null) {
2!
72
      version = LATEST;
×
73
    }
74
    if (!version.isPattern()) {
3✔
75
      for (VersionIdentifier vi : versions) {
10!
76
        if (vi.equals(version)) {
4✔
77
          logger.debug("Resolved version {} to version {}", version, vi);
13✔
78
          return vi;
2✔
79
        }
80
      }
1✔
81
    }
82
    for (VersionIdentifier vi : versions) {
10!
83
      if (version.contains(vi)) {
4✔
84
        logger.debug("Resolved version pattern {} to version {}", version, vi);
13✔
85
        return vi;
2✔
86
      }
87
    }
1✔
88
    throw new CliException(
×
89
        "Could not find any version matching '" + version + "' - there are " + versions.size() + " version(s) available but none matched!");
×
90
  }
91

92
  /**
93
   * @return the first {@link VersionSegment} of this {@link VersionIdentifier}. To get other segments use {@link VersionSegment#getNextOrEmpty()} or
94
   *     {@link VersionSegment#getNextOrNull()}.
95
   */
96
  public VersionSegment getStart() {
97

98
    return this.start;
3✔
99
  }
100

101
  /**
102
   * A valid {@link VersionIdentifier} has to meet the following requirements:
103
   * <ul>
104
   * <li>All {@link VersionSegment segments} themselves are {@link VersionSegment#isValid() valid}.</li>
105
   * <li>The {@link #getStart() start} {@link VersionSegment segment} shall have an {@link String#isEmpty() empty}
106
   * {@link VersionSegment#getSeparator() separator} (e.g. ".1.0" or "-1-2" are not considered valid).</li>
107
   * <li>The {@link #getStart() start} {@link VersionSegment segment} shall have an {@link String#isEmpty() empty}
108
   * {@link VersionSegment#getLettersString() letter-sequence} (e.g. "RC1" or "beta" are not considered valid).</li>
109
   * <li>Have at least one {@link VersionSegment segment} with a positive {@link VersionSegment#getNumber() number}
110
   * (e.g. "0.0.0" or "0.alpha" are not considered valid).</li>
111
   * <li>Have at max one {@link VersionSegment segment} with a {@link VersionSegment#getPhase() phase} that is a real
112
   * {@link VersionPhase#isDevelopmentPhase() development phase} (e.g. "1.alpha1.beta2" or "1.0.rc1-milestone2" are not
113
   * considered valid).</li>
114
   * <li>It is NOT a {@link #isPattern() pattern}.</li>
115
   * </ul>
116
   */
117
  @Override
118
  public boolean isValid() {
119

120
    return this.valid;
3✔
121
  }
122

123
  @Override
124
  public boolean isPattern() {
125

126
    VersionSegment segment = this.start;
3✔
127
    while (segment != null) {
2✔
128
      if (segment.isPattern()) {
3✔
129
        return true;
2✔
130
      }
131
      segment = segment.getNextOrNull();
4✔
132
    }
133
    return false;
2✔
134
  }
135

136

137
  /**
138
   * @return {@code true} if this is a stable version, {@code false} otherwise.
139
   * @see VersionLetters#isStable()
140
   */
141
  public boolean isStable() {
142

143
    return this.developmentPhase.isStable();
4✔
144
  }
145

146
  /**
147
   * @return the {@link VersionLetters#isDevelopmentPhase() development phase} of this {@link VersionIdentifier}. Will be {@link VersionLetters#EMPTY} if no
148
   *     development phase is specified in any {@link VersionSegment} and will be {@link VersionLetters#UNDEFINED} if more than one
149
   *     {@link VersionLetters#isDevelopmentPhase() development phase} is specified (e.g. "1.0-alpha1.rc2").
150
   */
151
  public VersionLetters getDevelopmentPhase() {
152

153
    return this.developmentPhase;
3✔
154
  }
155

156
  @Override
157
  public VersionComparisonResult compareVersion(VersionIdentifier other) {
158

159
    if (other == null) {
2✔
160
      return VersionComparisonResult.GREATER_UNSAFE;
2✔
161
    }
162
    VersionSegment thisSegment = this.start;
3✔
163
    VersionSegment otherSegment = other.start;
3✔
164
    VersionComparisonResult result = null;
2✔
165
    boolean unsafe = false;
2✔
166
    boolean todo = true;
2✔
167
    do {
168
      result = thisSegment.compareVersion(otherSegment);
4✔
169
      if (result.isEqual()) {
3✔
170
        if (thisSegment.isEmpty() && otherSegment.isEmpty()) {
6!
171
          todo = false;
3✔
172
        } else if (result.isUnsafe()) {
3!
173
          unsafe = true;
×
174
        }
175
      } else {
176
        todo = false;
2✔
177
      }
178
      thisSegment = thisSegment.getNextOrEmpty();
3✔
179
      otherSegment = otherSegment.getNextOrEmpty();
3✔
180
    } while (todo);
2✔
181
    if (unsafe) {
2!
182
      return result.withUnsafe();
×
183
    }
184
    return result;
2✔
185
  }
186

187
  /**
188
   * @param other the {@link VersionIdentifier} to be matched.
189
   * @return {@code true} if this {@link VersionIdentifier} is equal to the given {@link VersionIdentifier} or this {@link VersionIdentifier} is a pattern
190
   *     version (e.g. "17*" or "17.*") and the given {@link VersionIdentifier} matches to that pattern.
191
   */
192
  public boolean matches(VersionIdentifier other) {
193

194
    if (other == null) {
2✔
195
      return false;
2✔
196
    }
197
    VersionSegment thisSegment = this.start;
3✔
198
    VersionSegment otherSegment = other.start;
3✔
199
    while (true) {
200
      VersionMatchResult matchResult = thisSegment.matches(otherSegment);
4✔
201
      if (matchResult == VersionMatchResult.MATCH) {
3✔
202
        return true;
2✔
203
      } else if (matchResult == VersionMatchResult.MISMATCH) {
3✔
204
        return false;
2✔
205
      }
206
      thisSegment = thisSegment.getNextOrEmpty();
3✔
207
      otherSegment = otherSegment.getNextOrEmpty();
3✔
208
    }
1✔
209
  }
210

211
  /**
212
   * Increment the specified segment. For examples see {@code VersionIdentifierTest.testIncrement()}.
213
   *
214
   * @param digitNumber the index of the {@link VersionSegment} to increment. All segments before will remain untouched and all following segments will be
215
   *     set to zero.
216
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
217
   * @return the incremented {@link VersionIdentifier}.
218
   */
219
  public VersionIdentifier incrementSegment(int digitNumber, boolean keepLetters) {
220

221
    if (isPattern()) {
3!
222
      throw new IllegalStateException("Cannot increment version pattern: " + toString());
×
223
    }
224
    VersionSegment newStart = this.start.increment(digitNumber, keepLetters);
6✔
225
    return new VersionIdentifier(newStart);
5✔
226
  }
227

228
  /**
229
   * Increment the first digit (major version).
230
   *
231
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
232
   * @return the incremented {@link VersionIdentifier}.
233
   * @see #incrementSegment(int, boolean)
234
   */
235
  public VersionIdentifier incrementMajor(boolean keepLetters) {
236
    return incrementSegment(0, keepLetters);
5✔
237
  }
238

239
  /**
240
   * Increment the second digit (minor version).
241
   *
242
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
243
   * @return the incremented {@link VersionIdentifier}.
244
   * @see #incrementSegment(int, boolean)
245
   */
246
  public VersionIdentifier incrementMinor(boolean keepLetters) {
247
    return incrementSegment(1, keepLetters);
5✔
248
  }
249

250
  /**
251
   * Increment the third digit (patch or micro version).
252
   *
253
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
254
   * @return the incremented {@link VersionIdentifier}.
255
   * @see #incrementSegment(int, boolean)
256
   */
257
  public VersionIdentifier incrementPatch(boolean keepLetters) {
258
    return incrementSegment(2, keepLetters);
5✔
259
  }
260

261
  /**
262
   * Increment the last segment.
263
   *
264
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
265
   * @return the incremented {@link VersionIdentifier}.
266
   * @see #incrementSegment(int, boolean)
267
   */
268
  public VersionIdentifier incrementLastDigit(boolean keepLetters) {
269

270
    return incrementSegment(this.start.countDigits() - 1, keepLetters);
9✔
271
  }
272

273
  @Override
274
  public VersionIdentifier getMin() {
275

276
    return this;
×
277
  }
278

279
  @Override
280
  public VersionIdentifier getMax() {
281

282
    return this;
×
283
  }
284

285
  @Override
286
  public boolean contains(VersionIdentifier version) {
287

288
    return matches(version);
4✔
289
  }
290

291
  @Override
292
  public int hashCode() {
293

294
    VersionSegment segment = this.start;
×
295
    int hash = 1;
×
296
    while (segment != null) {
×
297
      hash = hash * 31 + segment.hashCode();
×
298
      segment = segment.getNextOrNull();
×
299
    }
300
    return hash;
×
301
  }
302

303
  @Override
304
  public boolean equals(Object obj) {
305

306
    if (obj == this) {
3✔
307
      return true;
2✔
308
    } else if (!(obj instanceof VersionIdentifier)) {
3✔
309
      return false;
2✔
310
    }
311
    VersionIdentifier other = (VersionIdentifier) obj;
3✔
312
    return Objects.equals(this.start, other.start);
6✔
313
  }
314

315
  @Override
316
  @JsonSerialize
317
  public String toString() {
318

319
    StringBuilder sb = new StringBuilder();
4✔
320
    VersionSegment segment = this.start;
3✔
321
    while (segment != null) {
2✔
322
      sb.append(segment.toString());
5✔
323
      segment = segment.getNextOrNull();
4✔
324
    }
325
    return sb.toString();
3✔
326
  }
327

328
  /**
329
   * @param version the {@link #toString() string representation} of the {@link VersionIdentifier} to parse.
330
   * @return the parsed {@link VersionIdentifier}.
331
   */
332
  @JsonCreator
333
  public static VersionIdentifier of(String version) {
334

335
    if (version == null) {
2✔
336
      return null;
2✔
337
    } else if (version.equals("latest") || version.equals("*")) {
8!
338
      return VersionIdentifier.LATEST;
2✔
339
    }
340
    VersionSegment startSegment = VersionSegment.of(version);
3✔
341
    if (startSegment == null) {
2✔
342
      return null;
2✔
343
    }
344
    return new VersionIdentifier(startSegment);
5✔
345
  }
346

347
  /**
348
   * @param v1 the first {@link VersionIdentifier}.
349
   * @param v2 the second {@link VersionIdentifier}.
350
   * @param treatNullAsNegativeInfinity {@code true} to treat {@code null} as negative infinity, {@code false} otherwise (positive infinity).
351
   * @return the null-safe {@link #compareVersion(VersionIdentifier) comparison} of the two {@link VersionIdentifier}s.
352
   */
353
  public static VersionComparisonResult compareVersion(VersionIdentifier v1, VersionIdentifier v2, boolean treatNullAsNegativeInfinity) {
354

355
    if (v1 == null) {
2✔
356
      if (v2 == null) {
2!
357
        return VersionComparisonResult.EQUAL;
×
358
      } else if (treatNullAsNegativeInfinity) {
2✔
359
        return VersionComparisonResult.LESS;
2✔
360
      }
361
      return VersionComparisonResult.GREATER;
2✔
362
    } else if (v2 == null) {
2✔
363
      if (treatNullAsNegativeInfinity) {
2✔
364
        return VersionComparisonResult.GREATER;
2✔
365
      }
366
      return VersionComparisonResult.LESS;
2✔
367
    }
368
    return v1.compareVersion(v2);
4✔
369
  }
370

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