View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.artifact.versioning;
20  
21  import java.math.BigInteger;
22  import java.util.ArrayDeque;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Deque;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Map;
31  
32  /**
33   * <p>
34   * Generic implementation of version comparison.
35   * </p>
36   *
37   * Features:
38   * <ul>
39   * <li>mixing of '<code>-</code>' (hyphen) and '<code>.</code>' (dot) separators,</li>
40   * <li>transition between characters and digits also constitutes a separator:
41   *     <code>1.0alpha1 =&gt; [1, 0, alpha, 1]</code></li>
42   * <li>unlimited number of version components,</li>
43   * <li>version components in the text can be digits or strings,</li>
44   * <li>
45   *   String qualifiers are ordered lexically (case insensitive), with the following exceptions:
46   *   <ul>
47   *     <li> 'snapshot' &lt; '' &lt; 'sp' </li>
48   *   </ul>
49   *   and alias -&gt; replacement (all case insensitive):
50   *   <ul>
51   *     <li> 'a' -&gt; 'alpha' </li>
52   *     <li> 'b' -&gt; 'beta' </li>
53   *     <li> 'm' -&gt; 'milestone' </li>
54   *     <li> 'cr' -&gt; 'rc' </li>
55   *     <li> 'final' -&gt; '' </li>
56   *     <li> 'final' -&gt; '' </li>
57   *     <li> 'final' -&gt; '' </li>
58   *   </ul>
59   * </li>
60   * <li>
61   *   Following semver rules is encouraged, and some qualifiers are discouraged (no matter the case):
62   *   <ul>
63   *     <li> The usage of 'CR' qualifier is discouraged. Use 'RC' instead. </li>
64   *     <li> The usage of 'final', 'ga', and 'release' qualifiers is discouraged. Use no qualifier instead. </li>
65   *     <li> The usage of 'SP' qualifier is discouraged. Increment the patch version instead. </li>
66   *   </ul>
67   *   For other qualifiers, natural ordering is used (case insensitive):
68   *   <ul>
69   *     <li> alpha = a &lt; beta = b &lt; milestone = m &lt; rc = cr &lt; snapshot &lt; '' = final = ga = release &lt; sp </li>
70   *   </ul>
71   * </li>
72   * <li>a hyphen usually precedes a qualifier, and is always less important than digits/number, for example
73   *   1.0.RC2 &lt; 1.0-RC3 &lt; 1.0.1 ; but prefer '1.0.0-RC1' over '1.0.0.RC1' </li>
74   * </ul>
75   *
76   * @see <a href="https://maven.apache.org/pom.html#Version_Order_Specification">Version Order Specification</a>
77   * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
78   * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
79   */
80  public class ComparableVersion implements Comparable<ComparableVersion> {
81      private static final int MAX_INTITEM_LENGTH = 9;
82  
83      private static final int MAX_LONGITEM_LENGTH = 18;
84  
85      private String value;
86  
87      private String canonical;
88  
89      private ListItem items;
90  
91      private interface Item {
92          int INT_ITEM = 3;
93          int LONG_ITEM = 4;
94          int BIGINTEGER_ITEM = 0;
95          int STRING_ITEM = 1;
96          int LIST_ITEM = 2;
97  
98          int compareTo(Item item);
99  
100         int getType();
101 
102         boolean isNull();
103     }
104 
105     /**
106      * Represents a numeric item in the version item list that can be represented with an int.
107      */
108     private static class IntItem implements Item {
109         private final int value;
110 
111         public static final IntItem ZERO = new IntItem();
112 
113         private IntItem() {
114             this.value = 0;
115         }
116 
117         IntItem(String str) {
118             this.value = Integer.parseInt(str);
119         }
120 
121         @Override
122         public int getType() {
123             return INT_ITEM;
124         }
125 
126         @Override
127         public boolean isNull() {
128             return value == 0;
129         }
130 
131         @Override
132         public int compareTo(Item item) {
133             if (item == null) {
134                 return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1
135             }
136 
137             switch (item.getType()) {
138                 case INT_ITEM:
139                     int itemValue = ((IntItem) item).value;
140                     return Integer.compare(value, itemValue);
141                 case LONG_ITEM:
142                 case BIGINTEGER_ITEM:
143                     return -1;
144 
145                 case STRING_ITEM:
146                     return 1; // 1.1 > 1-sp
147 
148                 case LIST_ITEM:
149                     return 1; // 1.1 > 1-1
150 
151                 default:
152                     throw new IllegalStateException("invalid item: " + item.getClass());
153             }
154         }
155 
156         @Override
157         public boolean equals(Object o) {
158             if (this == o) {
159                 return true;
160             }
161             if (o == null || getClass() != o.getClass()) {
162                 return false;
163             }
164 
165             IntItem intItem = (IntItem) o;
166 
167             return value == intItem.value;
168         }
169 
170         @Override
171         public int hashCode() {
172             return value;
173         }
174 
175         @Override
176         public String toString() {
177             return Integer.toString(value);
178         }
179     }
180 
181     /**
182      * Represents a numeric item in the version item list that can be represented with a long.
183      */
184     private static class LongItem implements Item {
185         private final long value;
186 
187         LongItem(String str) {
188             this.value = Long.parseLong(str);
189         }
190 
191         @Override
192         public int getType() {
193             return LONG_ITEM;
194         }
195 
196         @Override
197         public boolean isNull() {
198             return value == 0;
199         }
200 
201         @Override
202         public int compareTo(Item item) {
203             if (item == null) {
204                 return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1
205             }
206 
207             switch (item.getType()) {
208                 case INT_ITEM:
209                     return 1;
210                 case LONG_ITEM:
211                     long itemValue = ((LongItem) item).value;
212                     return Long.compare(value, itemValue);
213                 case BIGINTEGER_ITEM:
214                     return -1;
215 
216                 case STRING_ITEM:
217                     return 1; // 1.1 > 1-sp
218 
219                 case LIST_ITEM:
220                     return 1; // 1.1 > 1-1
221 
222                 default:
223                     throw new IllegalStateException("invalid item: " + item.getClass());
224             }
225         }
226 
227         @Override
228         public boolean equals(Object o) {
229             if (this == o) {
230                 return true;
231             }
232             if (o == null || getClass() != o.getClass()) {
233                 return false;
234             }
235 
236             LongItem longItem = (LongItem) o;
237 
238             return value == longItem.value;
239         }
240 
241         @Override
242         public int hashCode() {
243             return (int) (value ^ (value >>> 32));
244         }
245 
246         @Override
247         public String toString() {
248             return Long.toString(value);
249         }
250     }
251 
252     /**
253      * Represents a numeric item in the version item list.
254      */
255     private static class BigIntegerItem implements Item {
256         private final BigInteger value;
257 
258         BigIntegerItem(String str) {
259             this.value = new BigInteger(str);
260         }
261 
262         @Override
263         public int getType() {
264             return BIGINTEGER_ITEM;
265         }
266 
267         @Override
268         public boolean isNull() {
269             return BigInteger.ZERO.equals(value);
270         }
271 
272         @Override
273         public int compareTo(Item item) {
274             if (item == null) {
275                 return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1
276             }
277 
278             switch (item.getType()) {
279                 case INT_ITEM:
280                 case LONG_ITEM:
281                     return 1;
282 
283                 case BIGINTEGER_ITEM:
284                     return value.compareTo(((BigIntegerItem) item).value);
285 
286                 case STRING_ITEM:
287                     return 1; // 1.1 > 1-sp
288 
289                 case LIST_ITEM:
290                     return 1; // 1.1 > 1-1
291 
292                 default:
293                     throw new IllegalStateException("invalid item: " + item.getClass());
294             }
295         }
296 
297         @Override
298         public boolean equals(Object o) {
299             if (this == o) {
300                 return true;
301             }
302             if (o == null || getClass() != o.getClass()) {
303                 return false;
304             }
305 
306             BigIntegerItem that = (BigIntegerItem) o;
307 
308             return value.equals(that.value);
309         }
310 
311         @Override
312         public int hashCode() {
313             return value.hashCode();
314         }
315 
316         public String toString() {
317             return value.toString();
318         }
319     }
320 
321     /**
322      * Represents a string in the version item list, usually a qualifier.
323      */
324     private static class StringItem implements Item {
325         private static final List<String> QUALIFIERS = Arrays.asList("snapshot", "", "sp");
326 
327         private static final Map<String, String> ALIASES = new HashMap<>(4);
328 
329         static {
330             ALIASES.put("cr", "rc");
331             ALIASES.put("final", "");
332             ALIASES.put("ga", "");
333             ALIASES.put("release", "");
334         }
335 
336         /**
337          * An index value for the empty-string qualifier. This one is used to determine if a given qualifier makes
338          * the version older than one without a qualifier, or more recent.
339          */
340         private static final int RELEASE_VERSION_INDEX = QUALIFIERS.indexOf("");
341 
342         private final String value;
343 
344         StringItem(String value, boolean followedByDigit) {
345             if (followedByDigit && value.length() == 1) {
346                 // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
347                 switch (value.charAt(0)) {
348                     case 'a':
349                         value = "alpha";
350                         break;
351                     case 'b':
352                         value = "beta";
353                         break;
354                     case 'm':
355                         value = "milestone";
356                         break;
357                     default:
358                 }
359             }
360             this.value = ALIASES.getOrDefault(value, value);
361         }
362 
363         @Override
364         public int getType() {
365             return STRING_ITEM;
366         }
367 
368         @Override
369         public boolean isNull() {
370             return QUALIFIERS.indexOf(value) == RELEASE_VERSION_INDEX;
371         }
372 
373         /**
374          * Returns a comparable value for a qualifier.
375          *
376          * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical
377          * ordering.
378          *
379          * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1
380          * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character,
381          * so this is still fast. If more characters are needed then it requires a lexical sort anyway.
382          *
383          * @param qualifier
384          * @return an equivalent value that can be used with lexical comparison
385          * @deprecated Use {@link #compareQualifiers(String, String)} instead
386          */
387         @Deprecated
388         public static String comparableQualifier(String qualifier) {
389             int index = QUALIFIERS.indexOf(qualifier) + 1;
390 
391             return index == 0 ? ("0-" + qualifier) : String.valueOf(index);
392         }
393 
394         /**
395          * Compare the qualifiers of two artifact versions.
396          *
397          * @param qualifier1 qualifier of first artifact
398          * @param qualifier2 qualifier of second artifact
399          * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or
400          * greater than the second
401          */
402         public static int compareQualifiers(String qualifier1, String qualifier2) {
403             int i1 = QUALIFIERS.indexOf(qualifier1);
404             int i2 = QUALIFIERS.indexOf(qualifier2);
405 
406             // if both pre-release, then use natural lexical ordering
407             if (i1 == -1 && i2 == -1) {
408                 // alpha < beta < ea < milestone < preview < rc
409                 return qualifier1.compareTo(qualifier2);
410             }
411 
412             // 'other qualifier' < 'snapshot' < '' < 'sp'
413             return Integer.compare(i1, i2);
414         }
415 
416         @Override
417         public int compareTo(Item item) {
418             if (item == null) {
419                 // 1-rc < 1, 1-ga > 1
420                 return Integer.compare(QUALIFIERS.indexOf(value), RELEASE_VERSION_INDEX);
421             }
422             switch (item.getType()) {
423                 case INT_ITEM:
424                 case LONG_ITEM:
425                 case BIGINTEGER_ITEM:
426                     return -1; // 1.any < 1.1 ?
427 
428                 case STRING_ITEM:
429                     return compareQualifiers(value, ((StringItem) item).value);
430 
431                 case LIST_ITEM:
432                     return -1; // 1.any < 1-1
433 
434                 default:
435                     throw new IllegalStateException("invalid item: " + item.getClass());
436             }
437         }
438 
439         @Override
440         public boolean equals(Object o) {
441             if (this == o) {
442                 return true;
443             }
444             if (o == null || getClass() != o.getClass()) {
445                 return false;
446             }
447 
448             StringItem that = (StringItem) o;
449 
450             return value.equals(that.value);
451         }
452 
453         @Override
454         public int hashCode() {
455             return value.hashCode();
456         }
457 
458         public String toString() {
459             return value;
460         }
461     }
462 
463     /**
464      * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
465      * with '-(number)' in the version specification).
466      */
467     private static class ListItem extends ArrayList<Item> implements Item {
468         @Override
469         public int getType() {
470             return LIST_ITEM;
471         }
472 
473         @Override
474         public boolean isNull() {
475             return (size() == 0);
476         }
477 
478         void normalize() {
479             for (int i = size() - 1; i >= 0; i--) {
480                 Item lastItem = get(i);
481 
482                 if (lastItem.isNull()) {
483                     // remove null trailing items: 0, "", empty list
484                     remove(i);
485                 } else if (!(lastItem instanceof ListItem)) {
486                     break;
487                 }
488             }
489         }
490 
491         @Override
492         public int compareTo(Item item) {
493             if (item == null) {
494                 if (size() == 0) {
495                     return 0; // 1-0 = 1- (normalize) = 1
496                 }
497                 // Compare the entire list of items with null - not just the first one, MNG-6964
498                 for (Item i : this) {
499                     int result = i.compareTo(null);
500                     if (result != 0) {
501                         return result;
502                     }
503                 }
504                 return 0;
505             }
506             switch (item.getType()) {
507                 case INT_ITEM:
508                 case LONG_ITEM:
509                 case BIGINTEGER_ITEM:
510                     return -1; // 1-1 < 1.0.x
511 
512                 case STRING_ITEM:
513                     return 1; // 1-1 > 1-sp
514 
515                 case LIST_ITEM:
516                     Iterator<Item> left = iterator();
517                     Iterator<Item> right = ((ListItem) item).iterator();
518 
519                     while (left.hasNext() || right.hasNext()) {
520                         Item l = left.hasNext() ? left.next() : null;
521                         Item r = right.hasNext() ? right.next() : null;
522 
523                         // if this is shorter, then invert the compare and mul with -1
524                         int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r);
525 
526                         if (result != 0) {
527                             return result;
528                         }
529                     }
530 
531                     return 0;
532 
533                 default:
534                     throw new IllegalStateException("invalid item: " + item.getClass());
535             }
536         }
537 
538         @Override
539         public String toString() {
540             StringBuilder buffer = new StringBuilder();
541             for (Item item : this) {
542                 if (buffer.length() > 0) {
543                     buffer.append((item instanceof ListItem) ? '-' : '.');
544                 }
545                 buffer.append(item);
546             }
547             return buffer.toString();
548         }
549 
550         /**
551          * Return the contents in the same format that is used when you call toString() on a List.
552          */
553         private String toListString() {
554             StringBuilder buffer = new StringBuilder();
555             buffer.append("[");
556             for (Item item : this) {
557                 if (buffer.length() > 1) {
558                     buffer.append(", ");
559                 }
560                 if (item instanceof ListItem) {
561                     buffer.append(((ListItem) item).toListString());
562                 } else {
563                     buffer.append(item);
564                 }
565             }
566             buffer.append("]");
567             return buffer.toString();
568         }
569     }
570 
571     public ComparableVersion(String version) {
572         parseVersion(version);
573     }
574 
575     @SuppressWarnings("checkstyle:innerassignment")
576     public final void parseVersion(String version) {
577         this.value = version;
578 
579         items = new ListItem();
580 
581         version = version.toLowerCase(Locale.ENGLISH);
582 
583         ListItem list = items;
584 
585         Deque<Item> stack = new ArrayDeque<>();
586         stack.push(list);
587 
588         boolean isDigit = false;
589 
590         int startIndex = 0;
591 
592         for (int i = 0; i < version.length(); i++) {
593             char c = version.charAt(i);
594 
595             if (c == '.') {
596                 if (i == startIndex) {
597                     list.add(IntItem.ZERO);
598                 } else {
599                     list.add(parseItem(isDigit, version.substring(startIndex, i)));
600                 }
601                 startIndex = i + 1;
602             } else if (c == '-') {
603                 if (i == startIndex) {
604                     list.add(IntItem.ZERO);
605                 } else {
606                     list.add(parseItem(isDigit, version.substring(startIndex, i)));
607                 }
608                 startIndex = i + 1;
609 
610                 list.add(list = new ListItem());
611                 stack.push(list);
612             } else if (Character.isDigit(c)) {
613                 if (!isDigit && i > startIndex) {
614                     // 1.0.0.RC1 < 1.0.0-RC2
615                     // treat .RC as -RC
616                     if (!list.isEmpty()) {
617                         list.add(list = new ListItem());
618                         stack.push(list);
619                     }
620 
621                     list.add(new StringItem(version.substring(startIndex, i), true));
622                     startIndex = i;
623 
624                     list.add(list = new ListItem());
625                     stack.push(list);
626                 }
627 
628                 isDigit = true;
629             } else {
630                 if (isDigit && i > startIndex) {
631                     list.add(parseItem(true, version.substring(startIndex, i)));
632                     startIndex = i;
633 
634                     list.add(list = new ListItem());
635                     stack.push(list);
636                 }
637 
638                 isDigit = false;
639             }
640         }
641 
642         if (version.length() > startIndex) {
643             // 1.0.0.RC1 < 1.0.0-RC2
644             // treat .RC as -RC
645             if (!isDigit && !list.isEmpty()) {
646                 list.add(list = new ListItem());
647                 stack.push(list);
648             }
649 
650             list.add(parseItem(isDigit, version.substring(startIndex)));
651         }
652 
653         while (!stack.isEmpty()) {
654             list = (ListItem) stack.pop();
655             list.normalize();
656         }
657     }
658 
659     private static Item parseItem(boolean isDigit, String buf) {
660         if (isDigit) {
661             buf = stripLeadingZeroes(buf);
662             if (buf.length() <= MAX_INTITEM_LENGTH) {
663                 // lower than 2^31
664                 return new IntItem(buf);
665             } else if (buf.length() <= MAX_LONGITEM_LENGTH) {
666                 // lower than 2^63
667                 return new LongItem(buf);
668             }
669             return new BigIntegerItem(buf);
670         }
671         return new StringItem(buf, false);
672     }
673 
674     private static String stripLeadingZeroes(String buf) {
675         if (buf == null || buf.isEmpty()) {
676             return "0";
677         }
678         for (int i = 0; i < buf.length(); ++i) {
679             char c = buf.charAt(i);
680             if (c != '0') {
681                 return buf.substring(i);
682             }
683         }
684         return buf;
685     }
686 
687     @Override
688     public int compareTo(ComparableVersion o) {
689         return items.compareTo(o.items);
690     }
691 
692     @Override
693     public String toString() {
694         return value;
695     }
696 
697     public String getCanonical() {
698         if (canonical == null) {
699             canonical = items.toString();
700         }
701         return canonical;
702     }
703 
704     @Override
705     public boolean equals(Object o) {
706         return (o instanceof ComparableVersion) && items.equals(((ComparableVersion) o).items);
707     }
708 
709     @Override
710     public int hashCode() {
711         return items.hashCode();
712     }
713 
714     // CHECKSTYLE_OFF: LineLength
715     /**
716      * Main to test version parsing and comparison.
717      * <p>
718      * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue
719      * <pre>java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"</pre>
720      * command to command line. Result of given command will be something like this:
721      * <pre>
722      * Display parameters as parsed by Maven (in canonical form) and comparison result:
723      * 1. 1.2.7 == 1.2.7
724      *    1.2.7 &gt; 1.2-SNAPSHOT
725      * 2. 1.2-SNAPSHOT == 1.2-snapshot
726      * </pre>
727      *
728      * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always
729      * two adjacent will be compared
730      */
731     // CHECKSTYLE_ON: LineLength
732     public static void main(String... args) {
733         System.out.println("Display parameters as parsed by Maven (in canonical form and as a list of tokens) and"
734                 + " comparison result:");
735         if (args.length == 0) {
736             return;
737         }
738 
739         ComparableVersion prev = null;
740         int i = 1;
741         for (String version : args) {
742             ComparableVersion c = new ComparableVersion(version);
743 
744             if (prev != null) {
745                 int compare = prev.compareTo(c);
746                 System.out.println("   " + prev.toString() + ' ' + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">"))
747                         + ' ' + version);
748             }
749 
750             System.out.println(
751                     (i++) + ". " + version + " -> " + c.getCanonical() + "; tokens: " + c.items.toListString());
752 
753             prev = c;
754         }
755     }
756 }