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

devonfw / IDEasy / 19436066693

17 Nov 2025 04:03PM UTC coverage: 68.946% (+0.1%) from 68.834%
19436066693

push

github

web-flow
add support to increment versions (#1595)

Co-authored-by: Malte Brunnlieb <maybeec@users.noreply.github.com>

3520 of 5593 branches covered (62.94%)

Branch coverage included in aggregate %.

9204 of 12862 relevant lines covered (71.56%)

3.14 hits per line

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

90.38
cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java
1
package com.devonfw.tools.ide.version;
2

3
import java.util.Objects;
4

5
/**
6
 * Represents a single segment of a {@link VersionIdentifier}.
7
 */
8
public class VersionSegment implements VersionObject<VersionSegment> {
9

10
  /** Pattern to match a any version that matches the prefix. Value is: {@value} */
11
  public static final String PATTERN_MATCH_ANY_VERSION = "*!";
12

13
  /** Pattern to match a {@link VersionPhase#isStable() stable} version that matches the prefix. Value is: {@value} */
14
  public static final String PATTERN_MATCH_ANY_STABLE_VERSION = "*";
15

16
  private static final VersionSegment EMPTY = new VersionSegment("", "", "", "");
9✔
17

18
  private final String separator;
19

20
  private final VersionLetters letters;
21

22
  private final String pattern;
23

24
  private final String digits;
25

26
  private final int number;
27

28
  private VersionSegment next;
29

30
  /**
31
   * The constructor.
32
   *
33
   * @param separator the {@link #getSeparator() separator}.
34
   * @param letters the {@link #getLettersString() letters}.
35
   * @param digits the {@link #getDigits() digits}.
36
   */
37
  VersionSegment(String separator, String letters, String digits) {
38

39
    this(separator, letters, digits, "");
6✔
40
  }
1✔
41

42
  /**
43
   * The constructor.
44
   *
45
   * @param separator the {@link #getSeparator() separator}.
46
   * @param letters the {@link #getLettersString() letters}.
47
   * @param digits the {@link #getDigits() digits}.
48
   * @param pattern the {@link #getPattern() pattern}.
49
   */
50
  VersionSegment(String separator, String letters, String digits, String pattern) {
51

52
    super();
2✔
53
    this.separator = separator;
3✔
54
    this.letters = VersionLetters.of(letters);
4✔
55
    if (!pattern.isEmpty() && !PATTERN_MATCH_ANY_STABLE_VERSION.equals(pattern)
9✔
56
        && !PATTERN_MATCH_ANY_VERSION.equals(pattern)) {
2!
57
      throw new IllegalArgumentException("Invalid pattern: " + pattern);
×
58
    }
59
    this.pattern = pattern;
3✔
60
    this.digits = digits;
3✔
61
    if (this.digits.isEmpty()) {
4✔
62
      this.number = -1;
4✔
63
    } else {
64
      this.number = Integer.parseInt(this.digits);
5✔
65
    }
66
    if (EMPTY != null) {
2✔
67
      assert (!this.letters.isEmpty() || !this.digits.isEmpty() || !this.separator.isEmpty()
15✔
68
          || !this.pattern.isEmpty());
2!
69
    }
70
  }
1✔
71

72
  private VersionSegment(VersionSegment next, String separator, VersionLetters letters, String digits, int number, String pattern) {
73
    super();
2✔
74
    this.next = next;
3✔
75
    this.separator = separator;
3✔
76
    this.letters = letters;
3✔
77
    this.pattern = pattern;
3✔
78
    this.digits = digits;
3✔
79
    this.number = number;
3✔
80
  }
1✔
81

82
  /**
83
   * @return the separator {@link String} (e.g. "." or "-") or the empty {@link String} ("") for none.
84
   */
85
  public String getSeparator() {
86

87
    return this.separator;
3✔
88
  }
89

90
  /**
91
   * @return the letters or the empty {@link String} ("") for none. In canonical {@link VersionIdentifier}s letters indicate the development phase (e.g. "pre",
92
   *     "rc", "alpha", "beta", "milestone", "test", "dev", "SNAPSHOT", etc.). However, letters are technically any
93
   *     {@link Character#isLetter(char) letter characters} and may also be something like a code-name (e.g. "Cupcake", "Donut", "Eclair", "Froyo",
94
   *     "Gingerbread", "Honeycomb", "Ice Cream Sandwich", "Jelly Bean" in case of Android internals). Please note that in such case it is impossible to
95
   *     properly decide which version is greater than another versions. To avoid mistakes, the comparison supports a strict mode that will let the comparison
96
   *     fail in such case. However, by default (e.g. for {@link Comparable#compareTo(Object)}) the default {@link String#compareTo(String) string comparison}
97
   *     (lexicographical) is used to ensure a natural order.
98
   * @see #getPhase()
99
   */
100
  public String getLettersString() {
101

102
    return this.letters.getLetters();
4✔
103
  }
104

105
  /**
106
   * @return the {@link VersionLetters}.
107
   */
108
  public VersionLetters getLetters() {
109

110
    return this.letters;
3✔
111
  }
112

113
  /**
114
   * @return the {@link VersionPhase} for the {@link #getLettersString() letters}. Will be {@link VersionPhase#UNDEFINED} if unknown and hence never
115
   *     {@code null}.
116
   * @see #getLettersString()
117
   */
118
  public VersionPhase getPhase() {
119

120
    return this.letters.getPhase();
4✔
121
  }
122

123
  /**
124
   * @return the digits or the empty {@link String} ("") for none. This is the actual {@link #getNumber() number} part of this {@link VersionSegment}. So the
125
   *     {@link VersionIdentifier} "1.0.001" will have three segments: The first one with "1" as digits, the second with "0" as digits, and a third with "001"
126
   *     as digits. You can get the same value via {@link #getNumber()} but this {@link String} representation will preserve leading zeros.
127
   */
128
  public String getDigits() {
129

130
    return this.digits;
3✔
131
  }
132

133
  /**
134
   * @return the {@link #getDigits() digits} and integer number. Will be {@code -1} if no {@link #getDigits() digits} are present.
135
   */
136
  public int getNumber() {
137

138
    return this.number;
3✔
139
  }
140

141
  /**
142
   * @return the potential pattern that is {@link #PATTERN_MATCH_ANY_STABLE_VERSION}, {@link #PATTERN_MATCH_ANY_VERSION}, or for no pattern the empty
143
   *     {@link String}.
144
   */
145
  public String getPattern() {
146

147
    return this.pattern;
3✔
148
  }
149

150
  /**
151
   * @return {@code true} if {@link #getPattern() pattern} is NOT {@link String#isEmpty() empty}.
152
   */
153
  public boolean isPattern() {
154

155
    return !this.pattern.isEmpty();
8✔
156
  }
157

158
  /**
159
   * @return the next {@link VersionSegment} or {@code null} if this is the tail of the {@link VersionIdentifier}.
160
   */
161
  public VersionSegment getNextOrNull() {
162

163
    return this.next;
3✔
164
  }
165

166
  /**
167
   * @return the next {@link VersionSegment} or the {@link #ofEmpty() empty segment} if this is the tail of the {@link VersionIdentifier}.
168
   */
169
  public VersionSegment getNextOrEmpty() {
170

171
    if (this.next == null) {
3✔
172
      return EMPTY;
2✔
173
    }
174
    return this.next;
3✔
175
  }
176

177
  /**
178
   * @return {@code true} if this is the empty {@link VersionSegment}, {@code false} otherwise.
179
   */
180
  public boolean isEmpty() {
181

182
    return (this == EMPTY);
7✔
183
  }
184

185
  /**
186
   * A valid {@link VersionSegment} has to meet the following requirements:
187
   * <ul>
188
   * <li>The {@link #getSeparator() separator} may not be {@link String#length() longer} than a single character (e.g.
189
   * ".-_1" or "--1" are not considered valid).</li>
190
   * <li>The {@link #getSeparator() separator} may only contain the characters '.', '-', or '_' (e.g. " 1" or "ö1" are
191
   * not considered valid).</li>
192
   * <li>The combination of {@link #getPhase() phase} and {@link #getNumber() number} has to be
193
   * {@link VersionPhase#isValid(int) valid} (e.g. "pineapple-pen1" or "donut" are not considered valid).</li>
194
   * </ul>
195
   */
196
  @Override
197
  public boolean isValid() {
198

199
    if (!this.pattern.isEmpty()) {
4✔
200
      return false;
2✔
201
    }
202
    int separatorLen = this.separator.length();
4✔
203
    if (separatorLen > 1) {
3✔
204
      return false;
2✔
205
    } else if (separatorLen == 1) {
3✔
206
      if (!CharCategory.isValidSeparator(this.separator.charAt(0))) {
6✔
207
        return false;
2✔
208
      }
209
    }
210
    return this.letters.getPhase().isValid(this.number);
7✔
211
  }
212

213
  @Override
214
  public VersionComparisonResult compareVersion(VersionSegment other) {
215

216
    if (other == null) {
2!
217
      return VersionComparisonResult.GREATER_UNSAFE;
×
218
    }
219
    VersionComparisonResult lettersResult = this.letters.compareVersion(other.letters);
6✔
220
    if (!lettersResult.isEqual()) {
3✔
221
      return lettersResult;
2✔
222
    }
223
    if (!"_".equals(this.separator) && "_".equals(other.separator)) {
10✔
224
      if ("".equals(this.separator)) {
5!
225
        return VersionComparisonResult.LESS;
×
226
      } else {
227
        return VersionComparisonResult.GREATER;
2✔
228
      }
229
    } else if ("_".equals(this.separator) && !"_".equals(other.separator)) {
10!
230
      if ("".equals(other.separator)) {
5✔
231
        return VersionComparisonResult.GREATER;
2✔
232
      } else {
233
        return VersionComparisonResult.LESS;
2✔
234
      }
235
    }
236

237
    if (this.number != other.number) {
5✔
238
      if ((this.number < 0) && isPattern()) {
6✔
239
        return VersionComparisonResult.LESS_UNSAFE;
2✔
240
      } else if ((other.number < 0) && other.isPattern()) {
6!
241
        return VersionComparisonResult.GREATER_UNSAFE;
×
242
      } else if (this.number < other.number) {
5✔
243
        return VersionComparisonResult.LESS;
2✔
244
      } else {
245
        return VersionComparisonResult.GREATER;
2✔
246
      }
247
    } else if (this.separator.equals(other.separator)) {
6✔
248
      return VersionComparisonResult.EQUAL;
2✔
249
    } else {
250
      return VersionComparisonResult.EQUAL_UNSAFE;
2✔
251
    }
252
  }
253

254
  /**
255
   * Matches a {@link VersionSegment} with a potential {@link #getPattern() pattern} against another {@link VersionSegment}. This operation may not always be
256
   * symmetric.
257
   *
258
   * @param other the {@link VersionSegment} to match against.
259
   * @return the {@link VersionMatchResult} of the match.
260
   */
261
  public VersionMatchResult matches(VersionSegment other) {
262

263
    if (other == null) {
2!
264
      return VersionMatchResult.MISMATCH;
×
265
    }
266
    if (isEmpty() && other.isEmpty()) {
3!
267
      return VersionMatchResult.MATCH;
×
268
    }
269
    boolean isPattern = isPattern();
3✔
270
    if (isPattern) {
2✔
271
      if (!this.digits.isEmpty()) {
4✔
272
        if (this.number != other.number) {
5✔
273
          return VersionMatchResult.MISMATCH;
2✔
274
        }
275
      }
276
      if (!this.separator.isEmpty()) {
4✔
277
        if (!this.separator.equals(other.separator)) {
6✔
278
          return VersionMatchResult.MISMATCH;
2✔
279
        }
280
      }
281
    } else {
282
      if ((this.number != other.number) || !this.separator.equals(other.separator)) {
11!
283
        return VersionMatchResult.MISMATCH;
2✔
284
      }
285
    }
286
    VersionMatchResult result = this.letters.matches(other.letters, isPattern);
7✔
287
    if (isPattern && (result == VersionMatchResult.EQUAL)) {
5!
288
      if (this.pattern.equals(PATTERN_MATCH_ANY_STABLE_VERSION)) {
5✔
289
        VersionLetters developmentPhase = other.getDevelopmentPhase();
3✔
290
        if (developmentPhase.isUnstable()) {
3✔
291
          return VersionMatchResult.MISMATCH;
2✔
292
        }
293
        return VersionMatchResult.MATCH;
2✔
294
      } else if (this.pattern.equals(PATTERN_MATCH_ANY_VERSION)) {
5!
295
        return VersionMatchResult.MATCH;
2✔
296
      } else {
297
        throw new IllegalStateException("Pattern=" + this.pattern);
×
298
      }
299
    }
300
    return result;
2✔
301
  }
302

303
  /**
304
   * @return the {@link VersionLetters} that represent a {@link VersionLetters#isDevelopmentPhase() development phase} searching from this
305
   *     {@link VersionSegment} to all {@link #getNextOrNull() next segments}. Will be {@link VersionPhase#NONE} if no
306
   *     {@link VersionPhase#isDevelopmentPhase() development phase} was found and {@link VersionPhase#UNDEFINED} if multiple
307
   *     {@link VersionPhase#isDevelopmentPhase() development phase}s have been found.
308
   * @see VersionIdentifier#getDevelopmentPhase()
309
   */
310
  protected VersionLetters getDevelopmentPhase() {
311

312
    VersionLetters result = VersionLetters.EMPTY;
2✔
313
    VersionSegment segment = this;
2✔
314
    while (segment != null) {
2✔
315
      if (segment.letters.isDevelopmentPhase()) {
4✔
316
        if (result == VersionLetters.EMPTY) {
3!
317
          result = segment.letters;
4✔
318
        } else {
319
          result = VersionLetters.UNDEFINED;
×
320
        }
321
      }
322
      segment = segment.next;
4✔
323
    }
324
    return result;
2✔
325
  }
326

327
  /**
328
   * {@link VersionIdentifier#incrementSegment(int, boolean)}  Increments a version} recursively per {@link VersionSegment}.
329
   *
330
   * @param segmentKeepCount the number of leading {@link VersionSegment}s to keep untouched. Will be {@code 0} for the segment to increment and negative
331
   *     for the segments to set to zero.
332
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
333
   * @return the new {@link VersionSegment}.
334
   */
335
  VersionSegment increment(int segmentKeepCount, boolean keepLetters) {
336

337
    String separator = this.separator;
3✔
338
    VersionLetters letters = this.letters;
3✔
339
    String digits = this.digits;
3✔
340
    int number = this.number;
3✔
341
    String pattern = this.pattern;
3✔
342
    int nextSegmentKeepCount = segmentKeepCount;
2✔
343
    if (this.number >= 0) {
3✔
344
      nextSegmentKeepCount--;
1✔
345
    }
346
    if ((segmentKeepCount < 0) || ((segmentKeepCount == 0) && (this.number >= 0))) {
7✔
347
      if (!keepLetters) {
2✔
348
        letters = VersionLetters.EMPTY;
2✔
349
      }
350
      if (number >= 0) {
2✔
351
        if (segmentKeepCount == 0) {
2✔
352
          number++;
2✔
353
        } else {
354
          number = 0;
2✔
355
        }
356
        int digitsLength = digits.length();
3✔
357
        digits = Integer.toString(number);
3✔
358
        int leadingZeros = digitsLength - digits.length();
5✔
359
        if (leadingZeros > 0) {
2✔
360
          StringBuilder newDigits = new StringBuilder(digits);
5✔
361
          while (leadingZeros > 0) {
2✔
362
            newDigits.insert(0, "0");
5✔
363
            leadingZeros--;
2✔
364
          }
365
          digits = newDigits.toString();
3✔
366
        }
367
      } else if (!keepLetters) {
3✔
368
        if (this.next == null) {
3✔
369
          return null;
2✔
370
        }
371
        return this.next.increment(nextSegmentKeepCount, false);
6✔
372
      }
373
    }
374
    VersionSegment nextSegment = null;
2✔
375
    if (this.next != null) {
3✔
376
      nextSegment = this.next.increment(nextSegmentKeepCount, keepLetters);
6✔
377
    }
378
    return new VersionSegment(nextSegment, separator, letters, digits, number, pattern);
10✔
379
  }
380

381
  @Override
382
  public boolean equals(Object obj) {
383

384
    if (obj == this) {
3!
385
      return true;
×
386
    } else if (!(obj instanceof VersionSegment)) {
3✔
387
      return false;
2✔
388
    }
389
    VersionSegment other = (VersionSegment) obj;
3✔
390
    if (!Objects.equals(this.digits, other.digits)) {
6✔
391
      return false;
2✔
392
    } else if (!Objects.equals(this.separator, other.separator)) {
6!
393
      return false;
×
394
    } else if (!Objects.equals(this.letters, other.letters)) {
6!
395
      return false;
×
396
    } else if (!Objects.equals(this.pattern, other.pattern)) {
6!
397
      return false;
×
398
    } else if (!Objects.equals(this.next, other.next)) {
6✔
399
      return false;
2✔
400
    }
401
    return true;
2✔
402
  }
403

404
  @Override
405
  public String toString() {
406

407
    return this.separator + this.letters + this.digits + this.pattern;
11✔
408
  }
409

410
  /**
411
   * @return the {@link #isEmpty() empty} {@link VersionSegment} instance.
412
   */
413
  public static VersionSegment ofEmpty() {
414

415
    return EMPTY;
2✔
416
  }
417

418
  static VersionSegment of(String version) {
419

420
    CharReader reader = new CharReader(version);
5✔
421
    VersionSegment start = null;
2✔
422
    VersionSegment current = null;
2✔
423
    while (reader.hasNext()) {
3✔
424
      VersionSegment segment = parseSegment(reader);
3✔
425
      if (current == null) {
2✔
426
        start = segment;
3✔
427
      } else {
428
        current.next = segment;
3✔
429
      }
430
      current = segment;
2✔
431
    }
1✔
432
    return start;
2✔
433
  }
434

435
  private static VersionSegment parseSegment(CharReader reader) {
436

437
    String separator = reader.readSeparator();
3✔
438
    String letters = reader.readLetters();
3✔
439
    String digits = reader.readDigits();
3✔
440
    String pattern = reader.readPattern();
3✔
441
    return new VersionSegment(separator, letters, digits, pattern);
8✔
442
  }
443

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

© 2025 Coveralls, Inc