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

devonfw / IDEasy / 22303886886

23 Feb 2026 11:19AM UTC coverage: 70.647% (+0.2%) from 70.474%
22303886886

Pull #1714

github

web-flow
Merge f1f7e1e61 into 379acdc9d
Pull Request #1714: #404: #1713: advanced logging

4069 of 6360 branches covered (63.98%)

Branch coverage included in aggregate %.

10644 of 14466 relevant lines covered (73.58%)

3.1 hits per line

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

86.88
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 org.slf4j.Logger;
7
import org.slf4j.LoggerFactory;
8

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

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

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

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

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

28
  private final VersionSegment start;
29

30
  private final VersionLetters developmentPhase;
31

32
  private final boolean valid;
33

34
  private VersionIdentifier(VersionSegment start) {
35

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

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

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

101
    return this.start;
3✔
102
  }
103

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

123
    return this.valid;
3✔
124
  }
125

126
  @Override
127
  public boolean isPattern() {
128

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

139

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

146
    return this.developmentPhase.isStable();
4✔
147
  }
148

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

156
    return this.developmentPhase;
3✔
157
  }
158

159
  @Override
160
  public VersionComparisonResult compareVersion(VersionIdentifier other) {
161

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

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

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

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

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

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

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

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

264
  /**
265
   * Increment the last segment.
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 incrementLastDigit(boolean keepLetters) {
272

273
    return incrementSegment(this.start.countDigits() - 1, keepLetters);
9✔
274
  }
275

276
  @Override
277
  public VersionIdentifier getMin() {
278

279
    return this;
×
280
  }
281

282
  @Override
283
  public VersionIdentifier getMax() {
284

285
    return this;
×
286
  }
287

288
  @Override
289
  public boolean contains(VersionIdentifier version) {
290

291
    return matches(version);
4✔
292
  }
293

294
  @Override
295
  public int hashCode() {
296

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

306
  @Override
307
  public boolean equals(Object obj) {
308

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

318
  @Override
319
  @JsonSerialize
320
  public String toString() {
321

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

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

338
    if (version == null) {
2✔
339
      return null;
2✔
340
    }
341
    version = version.trim();
3✔
342
    if (version.equals("latest") || version.equals("*")) {
8!
343
      return VersionIdentifier.LATEST;
2✔
344
    }
345
    assert !version.contains(" ") && !version.contains("\n") && !version.contains("\t") : version;
13!
346
    VersionSegment startSegment = VersionSegment.of(version);
3✔
347
    if (startSegment == null) {
2✔
348
      return null;
2✔
349
    }
350
    return new VersionIdentifier(startSegment);
5✔
351
  }
352

353
  /**
354
   * @param v1 the first {@link VersionIdentifier}.
355
   * @param v2 the second {@link VersionIdentifier}.
356
   * @param treatNullAsNegativeInfinity {@code true} to treat {@code null} as negative infinity, {@code false} otherwise (positive infinity).
357
   * @return the null-safe {@link #compareVersion(VersionIdentifier) comparison} of the two {@link VersionIdentifier}s.
358
   */
359
  public static VersionComparisonResult compareVersion(VersionIdentifier v1, VersionIdentifier v2, boolean treatNullAsNegativeInfinity) {
360

361
    if (v1 == null) {
2✔
362
      if (v2 == null) {
2!
363
        return VersionComparisonResult.EQUAL;
×
364
      } else if (treatNullAsNegativeInfinity) {
2✔
365
        return VersionComparisonResult.LESS;
2✔
366
      }
367
      return VersionComparisonResult.GREATER;
2✔
368
    } else if (v2 == null) {
2✔
369
      if (treatNullAsNegativeInfinity) {
2✔
370
        return VersionComparisonResult.GREATER;
2✔
371
      }
372
      return VersionComparisonResult.LESS;
2✔
373
    }
374
    return v1.compareVersion(v2);
4✔
375
  }
376

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