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

devonfw / IDEasy / 10141309149

29 Jul 2024 09:02AM UTC coverage: 61.174% (+0.03%) from 61.148%
10141309149

push

github

web-flow
#489: Fix separator comparison (#493)

2001 of 3599 branches covered (55.6%)

Branch coverage included in aggregate %.

5302 of 8339 relevant lines covered (63.58%)

2.8 hits per line

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

87.5
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
    /*
61
     * this.lettersLower = this.letters.toLowerCase(Locale.ROOT); String phaseLetters = this.lettersLower.replace('_',
62
     * '-'); if (phaseLetters.startsWith("pre")) { this.prePhase = true; int preLength = 3; if
63
     * (phaseLetters.startsWith("pre-")) { preLength = 4; } phaseLetters = phaseLetters.substring(preLength); } else {
64
     * this.prePhase = false; } this.phase = VersionPhase.of(phaseLetters);
65
     */
66
    this.digits = digits;
3✔
67
    if (this.digits.isEmpty()) {
4✔
68
      this.number = -1;
4✔
69
    } else {
70
      this.number = Integer.parseInt(this.digits);
5✔
71
    }
72
    if (EMPTY != null) {
2✔
73
      assert (!this.letters.isEmpty() || !this.digits.isEmpty() || !this.separator.isEmpty()
15✔
74
          || !this.pattern.isEmpty());
2!
75
    }
76
  }
1✔
77

78
  /**
79
   * @return the separator {@link String} (e.g. "." or "-") or the empty {@link String} ("") for none.
80
   */
81
  public String getSeparator() {
82

83
    return this.separator;
3✔
84
  }
85

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

98
    return this.letters.getLetters();
4✔
99
  }
100

101
  /**
102
   * @return the {@link VersionLetters}.
103
   */
104
  public VersionLetters getLetters() {
105

106
    return this.letters;
3✔
107
  }
108

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

116
    return this.letters.getPhase();
4✔
117
  }
118

119
  /**
120
   * @return the digits or the empty {@link String} ("") for none. This is the actual {@link #getNumber() number} part of this {@link VersionSegment}. So the
121
   * {@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" as
122
   * digits. You can get the same value via {@link #getNumber()} but this {@link String} representation will preserve leading zeros.
123
   */
124
  public String getDigits() {
125

126
    return this.digits;
3✔
127
  }
128

129
  /**
130
   * @return the {@link #getDigits() digits} and integer number. Will be {@code -1} if no {@link #getDigits() digits} are present.
131
   */
132
  public int getNumber() {
133

134
    return this.number;
3✔
135
  }
136

137
  /**
138
   * @return the potential pattern that is {@link #PATTERN_MATCH_ANY_STABLE_VERSION}, {@link #PATTERN_MATCH_ANY_VERSION}, or for no pattern the empty
139
   * {@link String}.
140
   */
141
  public String getPattern() {
142

143
    return this.pattern;
3✔
144
  }
145

146
  /**
147
   * @return {@code true} if {@link #getPattern() pattern} is NOT {@link String#isEmpty() empty}.
148
   */
149
  public boolean isPattern() {
150

151
    return !this.pattern.isEmpty();
8✔
152
  }
153

154
  /**
155
   * @return the next {@link VersionSegment} or {@code null} if this is the tail of the {@link VersionIdentifier}.
156
   */
157
  public VersionSegment getNextOrNull() {
158

159
    return this.next;
3✔
160
  }
161

162
  /**
163
   * @return the next {@link VersionSegment} or the {@link #ofEmpty() empty segment} if this is the tail of the {@link VersionIdentifier}.
164
   */
165
  public VersionSegment getNextOrEmpty() {
166

167
    if (this.next == null) {
3✔
168
      return EMPTY;
2✔
169
    }
170
    return this.next;
3✔
171
  }
172

173
  /**
174
   * @return {@code true} if this is the empty {@link VersionSegment}, {@code false} otherwise.
175
   */
176
  public boolean isEmpty() {
177

178
    return (this == EMPTY);
7✔
179
  }
180

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

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

209
  @Override
210
  public VersionComparisonResult compareVersion(VersionSegment other) {
211

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

233
    if (this.number < other.number) {
5✔
234
      return VersionComparisonResult.LESS;
2✔
235
    } else if (this.number > other.number) {
5✔
236
      return VersionComparisonResult.GREATER;
2✔
237
    } else if (this.separator.equals(other.separator)) {
6!
238
      return VersionComparisonResult.EQUAL;
2✔
239
    } else {
240
      return VersionComparisonResult.EQUAL_UNSAFE;
×
241
    }
242
  }
243

244
  /**
245
   * Matches a {@link VersionSegment} with a potential {@link #getPattern() pattern} against another {@link VersionSegment}. This operation may not always be
246
   * symmetric.
247
   *
248
   * @param other the {@link VersionSegment} to match against.
249
   * @return the {@link VersionMatchResult} of the match.
250
   */
251
  public VersionMatchResult matches(VersionSegment other) {
252

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

293
  /**
294
   * @return the {@link VersionLetters} that represent a {@link VersionLetters#isDevelopmentPhase() development phase} searching from this
295
   * {@link VersionSegment} to all {@link #getNextOrNull() next segments}. Will be {@link VersionPhase#NONE} if no
296
   * {@link VersionPhase#isDevelopmentPhase() development phase} was found and {@link VersionPhase#UNDEFINED} if multiple
297
   * {@link VersionPhase#isDevelopmentPhase() development phase}s have been found.
298
   * @see VersionIdentifier#getDevelopmentPhase()
299
   */
300
  protected VersionLetters getDevelopmentPhase() {
301

302
    VersionLetters result = VersionLetters.EMPTY;
2✔
303
    VersionSegment segment = this;
2✔
304
    while (segment != null) {
2✔
305
      if (segment.letters.isDevelopmentPhase()) {
4✔
306
        if (result == VersionLetters.EMPTY) {
3!
307
          result = segment.letters;
4✔
308
        } else {
309
          result = VersionLetters.UNDEFINED;
×
310
        }
311
      }
312
      segment = segment.next;
4✔
313
    }
314
    return result;
2✔
315
  }
316

317
  @Override
318
  public boolean equals(Object obj) {
319

320
    if (obj == this) {
3!
321
      return true;
×
322
    } else if (!(obj instanceof VersionSegment)) {
3✔
323
      return false;
2✔
324
    }
325
    VersionSegment other = (VersionSegment) obj;
3✔
326
    if (!Objects.equals(this.digits, other.digits)) {
6✔
327
      return false;
2✔
328
    } else if (!Objects.equals(this.separator, other.separator)) {
6!
329
      return false;
×
330
    } else if (!Objects.equals(this.letters, other.letters)) {
6!
331
      return false;
×
332
    } else if (!Objects.equals(this.pattern, other.pattern)) {
6!
333
      return false;
×
334
    } else if (!Objects.equals(this.next, other.next)) {
6✔
335
      return false;
2✔
336
    }
337
    return true;
2✔
338
  }
339

340
  @Override
341
  public String toString() {
342

343
    return this.separator + this.letters + this.digits + this.pattern;
10✔
344
  }
345

346
  /**
347
   * @return the {@link #isEmpty() empty} {@link VersionSegment} instance.
348
   */
349
  public static VersionSegment ofEmpty() {
350

351
    return EMPTY;
2✔
352
  }
353

354
  static VersionSegment of(String version) {
355

356
    CharReader reader = new CharReader(version);
5✔
357
    VersionSegment start = null;
2✔
358
    VersionSegment current = null;
2✔
359
    while (reader.hasNext()) {
3✔
360
      VersionSegment segment = parseSegment(reader);
3✔
361
      if (current == null) {
2✔
362
        start = segment;
3✔
363
      } else {
364
        if (!current.getPattern().isEmpty()) {
4✔
365
          throw new IllegalArgumentException("Invalid version pattern: " + version);
6✔
366
        }
367
        current.next = segment;
3✔
368
      }
369
      current = segment;
2✔
370
    }
1✔
371
    return start;
2✔
372
  }
373

374
  private static VersionSegment parseSegment(CharReader reader) {
375

376
    String separator = reader.readSeparator();
3✔
377
    String letters = reader.readLetters();
3✔
378
    String digits = reader.readDigits();
3✔
379
    String pattern = reader.readPattern();
3✔
380
    return new VersionSegment(separator, letters, digits, pattern);
8✔
381
  }
382

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