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.Iterator;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Objects;
30  import java.util.Properties;
31  
32  /**
33   * <p>
34   * Generic implementation of version comparison.
35   * </p>
36   * <p>
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, [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>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
45   *     Well-known qualifiers (case insensitive) are:<ul>
46   *     <li><code>alpha</code> or <code>a</code></li>
47   *     <li><code>beta</code> or <code>b</code></li>
48   *     <li><code>milestone</code> or <code>m</code></li>
49   *     <li><code>rc</code> or <code>cr</code></li>
50   *     <li><code>snapshot</code></li>
51   *     <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li>
52   *     <li><code>sp</code></li>
53   *     </ul>
54   *     Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive),
55   *   </li>
56   * <li>a hyphen usually precedes a qualifier, and is always less important than digits/number, for example
57   *   {@code 1.0.RC2 < 1.0-RC3 < 1.0.1}; but prefer {@code 1.0.0-RC1} over {@code 1.0.0.RC1}, and more
58   *   generally: {@code 1.0.X2 < 1.0-X3 < 1.0.1} for any string {@code X}; but prefer {@code 1.0.0-X1}
59   *   over {@code 1.0.0.X1}.</li>
60   * </ul>
61   *
62   * @see <a href="https://maven.apache.org/pom.html#version-order-specification">"Versioning" in the POM reference</a>
63   */
64  public class ComparableVersion implements Comparable<ComparableVersion> {
65      private static final int MAX_INTITEM_LENGTH = 9;
66  
67      private static final int MAX_LONGITEM_LENGTH = 18;
68  
69      private String value;
70  
71      private String canonical;
72  
73      private ListItem items;
74  
75      private interface Item {
76          int INT_ITEM = 3;
77          int LONG_ITEM = 4;
78          int BIGINTEGER_ITEM = 0;
79          int STRING_ITEM = 1;
80          int LIST_ITEM = 2;
81          int COMBINATION_ITEM = 5;
82  
83          int compareTo(Item item);
84  
85          int getType();
86  
87          boolean isNull();
88      }
89  
90      /**
91       * Represents a numeric item in the version item list that can be represented with an int.
92       */
93      private static class IntItem implements Item {
94          private final int value;
95  
96          public static final IntItem ZERO = new IntItem();
97  
98          private IntItem() {
99              this.value = 0;
100         }
101 
102         IntItem(String str) {
103             this.value = Integer.parseInt(str);
104         }
105 
106         @Override
107         public int getType() {
108             return INT_ITEM;
109         }
110 
111         @Override
112         public boolean isNull() {
113             return value == 0;
114         }
115 
116         @Override
117         public int compareTo(Item item) {
118             if (item == null) {
119                 return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1
120             }
121 
122             switch (item.getType()) {
123                 case INT_ITEM:
124                     int itemValue = ((IntItem) item).value;
125                     return Integer.compare(value, itemValue);
126                 case LONG_ITEM:
127                 case BIGINTEGER_ITEM:
128                     return -1;
129 
130                 case STRING_ITEM:
131                     return 1;
132                 case COMBINATION_ITEM:
133                     return 1; // 1.1 > 1-sp
134 
135                 case LIST_ITEM:
136                     return 1; // 1.1 > 1-1
137 
138                 default:
139                     throw new IllegalStateException("invalid item: " + item.getClass());
140             }
141         }
142 
143         @Override
144         public boolean equals(Object o) {
145             if (this == o) {
146                 return true;
147             }
148             if (o == null || getClass() != o.getClass()) {
149                 return false;
150             }
151 
152             IntItem intItem = (IntItem) o;
153 
154             return value == intItem.value;
155         }
156 
157         @Override
158         public int hashCode() {
159             return value;
160         }
161 
162         @Override
163         public String toString() {
164             return Integer.toString(value);
165         }
166     }
167 
168     /**
169      * Represents a numeric item in the version item list that can be represented with a long.
170      */
171     private static class LongItem implements Item {
172         private final long value;
173 
174         LongItem(String str) {
175             this.value = Long.parseLong(str);
176         }
177 
178         @Override
179         public int getType() {
180             return LONG_ITEM;
181         }
182 
183         @Override
184         public boolean isNull() {
185             return value == 0;
186         }
187 
188         @Override
189         public int compareTo(Item item) {
190             if (item == null) {
191                 return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1
192             }
193 
194             switch (item.getType()) {
195                 case INT_ITEM:
196                     return 1;
197                 case LONG_ITEM:
198                     long itemValue = ((LongItem) item).value;
199                     return Long.compare(value, itemValue);
200                 case BIGINTEGER_ITEM:
201                     return -1;
202 
203                 case STRING_ITEM:
204                     return 1;
205                 case COMBINATION_ITEM:
206                     return 1; // 1.1 > 1-sp
207 
208                 case LIST_ITEM:
209                     return 1; // 1.1 > 1-1
210 
211                 default:
212                     throw new IllegalStateException("invalid item: " + item.getClass());
213             }
214         }
215 
216         @Override
217         public boolean equals(Object o) {
218             if (this == o) {
219                 return true;
220             }
221             if (o == null || getClass() != o.getClass()) {
222                 return false;
223             }
224 
225             LongItem longItem = (LongItem) o;
226 
227             return value == longItem.value;
228         }
229 
230         @Override
231         public int hashCode() {
232             return (int) (value ^ (value >>> 32));
233         }
234 
235         @Override
236         public String toString() {
237             return Long.toString(value);
238         }
239     }
240 
241     /**
242      * Represents a numeric item in the version item list.
243      */
244     private static class BigIntegerItem implements Item {
245         private final BigInteger value;
246 
247         BigIntegerItem(String str) {
248             this.value = new BigInteger(str);
249         }
250 
251         @Override
252         public int getType() {
253             return BIGINTEGER_ITEM;
254         }
255 
256         @Override
257         public boolean isNull() {
258             return BigInteger.ZERO.equals(value);
259         }
260 
261         @Override
262         public int compareTo(Item item) {
263             if (item == null) {
264                 return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1
265             }
266 
267             switch (item.getType()) {
268                 case INT_ITEM:
269                 case LONG_ITEM:
270                     return 1;
271 
272                 case BIGINTEGER_ITEM:
273                     return value.compareTo(((BigIntegerItem) item).value);
274 
275                 case STRING_ITEM:
276                     return 1;
277                 case COMBINATION_ITEM:
278                     return 1; // 1.1 > 1-sp
279 
280                 case LIST_ITEM:
281                     return 1; // 1.1 > 1-1
282 
283                 default:
284                     throw new IllegalStateException("invalid item: " + item.getClass());
285             }
286         }
287 
288         @Override
289         public boolean equals(Object o) {
290             if (this == o) {
291                 return true;
292             }
293             if (o == null || getClass() != o.getClass()) {
294                 return false;
295             }
296 
297             BigIntegerItem that = (BigIntegerItem) o;
298 
299             return value.equals(that.value);
300         }
301 
302         @Override
303         public int hashCode() {
304             return value.hashCode();
305         }
306 
307         public String toString() {
308             return value.toString();
309         }
310     }
311 
312     /**
313      * Represents a string in the version item list, usually a qualifier.
314      */
315     private static class StringItem implements Item {
316         private static final List<String> QUALIFIERS =
317                 Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp");
318         private static final List<String> RELEASE_QUALIFIERS = Arrays.asList("ga", "final", "release");
319 
320         private static final Properties ALIASES = new Properties();
321 
322         static {
323             ALIASES.put("cr", "rc");
324         }
325 
326         /**
327          * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
328          * the version older than one without a qualifier, or more recent.
329          */
330         private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf(""));
331 
332         private final String value;
333 
334         StringItem(String value, boolean followedByDigit) {
335             if (followedByDigit && value.length() == 1) {
336                 // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
337                 switch (value.charAt(0)) {
338                     case 'a':
339                         value = "alpha";
340                         break;
341                     case 'b':
342                         value = "beta";
343                         break;
344                     case 'm':
345                         value = "milestone";
346                         break;
347                     default:
348                 }
349             }
350             this.value = ALIASES.getProperty(value, value);
351         }
352 
353         @Override
354         public int getType() {
355             return STRING_ITEM;
356         }
357 
358         @Override
359         public boolean isNull() {
360             return value == null || value.isEmpty();
361         }
362 
363         /**
364          * Returns a comparable value for a qualifier.
365          * <p>
366          * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical
367          * ordering.
368          * <p>
369          *
370          * @param qualifier
371          * @return an equivalent value that can be used with lexical comparison
372          */
373         public static String comparableQualifier(String qualifier) {
374             if (RELEASE_QUALIFIERS.contains(qualifier)) {
375                 return String.valueOf(QUALIFIERS.indexOf(""));
376             }
377 
378             int i = QUALIFIERS.indexOf(qualifier);
379 
380             // Just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for
381             // -1
382             //  or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first
383             // character,
384             // so this is still fast. If more characters are needed then it requires a lexical sort anyway.
385             return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i);
386         }
387 
388         @Override
389         public int compareTo(Item item) {
390             if (item == null) {
391                 // 1-rc < 1, 1-ga > 1
392                 return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
393             }
394             switch (item.getType()) {
395                 case INT_ITEM:
396                 case LONG_ITEM:
397                 case BIGINTEGER_ITEM:
398                     return -1; // 1.any < 1.1 ?
399 
400                 case STRING_ITEM:
401                     return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value));
402 
403                 case COMBINATION_ITEM:
404                     int result = this.compareTo(((CombinationItem) item).getStringPart());
405                     if (result == 0) {
406                         return -1;
407                     }
408                     return result;
409 
410                 case LIST_ITEM:
411                     return -1; // 1.any < 1-1
412 
413                 default:
414                     throw new IllegalStateException("invalid item: " + item.getClass());
415             }
416         }
417 
418         @Override
419         public boolean equals(Object o) {
420             if (this == o) {
421                 return true;
422             }
423             if (o == null || getClass() != o.getClass()) {
424                 return false;
425             }
426 
427             StringItem that = (StringItem) o;
428 
429             return value.equals(that.value);
430         }
431 
432         @Override
433         public int hashCode() {
434             return value.hashCode();
435         }
436 
437         public String toString() {
438             return value;
439         }
440     }
441 
442     /**
443      * Represents a combination in the version item list.
444      * It is usually a combination of a string and a number, with the string first and the number second.
445      */
446     private static class CombinationItem implements Item {
447 
448         StringItem stringPart;
449 
450         Item digitPart;
451 
452         CombinationItem(String value) {
453             int index = 0;
454             for (int i = 0; i < value.length(); i++) {
455                 char c = value.charAt(i);
456                 if (Character.isDigit(c)) {
457                     index = i;
458                     break;
459                 }
460             }
461 
462             stringPart = new StringItem(value.substring(0, index), true);
463             digitPart = parseItem(true, value.substring(index));
464         }
465 
466         @Override
467         public int compareTo(Item item) {
468             if (item == null) {
469                 // 1-rc1 < 1, 1-ga1 > 1
470                 return stringPart.compareTo(item);
471             }
472             int result = 0;
473             switch (item.getType()) {
474                 case INT_ITEM:
475                 case LONG_ITEM:
476                 case BIGINTEGER_ITEM:
477                     return -1;
478 
479                 case STRING_ITEM:
480                     result = stringPart.compareTo(item);
481                     if (result == 0) {
482                         // X1 > X
483                         return 1;
484                     }
485                     return result;
486 
487                 case LIST_ITEM:
488                     return -1;
489 
490                 case COMBINATION_ITEM:
491                     result = stringPart.compareTo(((CombinationItem) item).getStringPart());
492                     if (result == 0) {
493                         return digitPart.compareTo(((CombinationItem) item).getDigitPart());
494                     }
495                     return result;
496                 default:
497                     return 0;
498             }
499         }
500 
501         public StringItem getStringPart() {
502             return stringPart;
503         }
504 
505         public Item getDigitPart() {
506             return digitPart;
507         }
508 
509         @Override
510         public int getType() {
511             return COMBINATION_ITEM;
512         }
513 
514         @Override
515         public boolean isNull() {
516             return false;
517         }
518 
519         @Override
520         public boolean equals(Object o) {
521             if (this == o) {
522                 return true;
523             }
524             if (o == null || getClass() != o.getClass()) {
525                 return false;
526             }
527             CombinationItem that = (CombinationItem) o;
528             return Objects.equals(stringPart, that.stringPart) && Objects.equals(digitPart, that.digitPart);
529         }
530 
531         @Override
532         public int hashCode() {
533             return Objects.hash(stringPart, digitPart);
534         }
535 
536         @Override
537         public String toString() {
538             return stringPart.toString() + digitPart.toString();
539         }
540     }
541 
542     /**
543      * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
544      * with '-(number)' in the version specification).
545      */
546     private static class ListItem extends ArrayList<Item> implements Item {
547         @Override
548         public int getType() {
549             return LIST_ITEM;
550         }
551 
552         @Override
553         public boolean isNull() {
554             return (size() == 0);
555         }
556 
557         void normalize() {
558             for (int i = size() - 1; i >= 0; i--) {
559                 Item lastItem = get(i);
560 
561                 if (lastItem.isNull()) {
562                     if (i == size() - 1 || get(i + 1).getType() == STRING_ITEM) {
563                         remove(i);
564                     } else if (get(i + 1).getType() == LIST_ITEM) {
565                         Item item = ((ListItem) get(i + 1)).get(0);
566                         if (item.getType() == COMBINATION_ITEM || item.getType() == STRING_ITEM) {
567                             remove(i);
568                         }
569                     }
570                 }
571             }
572         }
573 
574         @Override
575         public int compareTo(Item item) {
576             if (item == null) {
577                 if (size() == 0) {
578                     return 0; // 1-0 = 1- (normalize) = 1
579                 }
580                 // Compare the entire list of items with null - not just the first one, MNG-6964
581                 for (Item i : this) {
582                     int result = i.compareTo(null);
583                     if (result != 0) {
584                         return result;
585                     }
586                 }
587                 return 0;
588             }
589             switch (item.getType()) {
590                 case INT_ITEM:
591                 case LONG_ITEM:
592                 case BIGINTEGER_ITEM:
593                     return -1; // 1-1 < 1.0.x
594 
595                 case STRING_ITEM:
596                     return 1;
597                 case COMBINATION_ITEM:
598                     return 1; // 1-1 > 1-sp
599 
600                 case LIST_ITEM:
601                     Iterator<Item> left = iterator();
602                     Iterator<Item> right = ((ListItem) item).iterator();
603 
604                     while (left.hasNext() || right.hasNext()) {
605                         Item l = left.hasNext() ? left.next() : null;
606                         Item r = right.hasNext() ? right.next() : null;
607 
608                         // if this is shorter, then invert the compare and mul with -1
609                         int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r);
610 
611                         if (result != 0) {
612                             return result;
613                         }
614                     }
615 
616                     return 0;
617 
618                 default:
619                     throw new IllegalStateException("invalid item: " + item.getClass());
620             }
621         }
622 
623         @Override
624         public String toString() {
625             StringBuilder buffer = new StringBuilder();
626             for (Item item : this) {
627                 if (buffer.length() > 0) {
628                     buffer.append((item instanceof ListItem) ? '-' : '.');
629                 }
630                 buffer.append(item);
631             }
632             return buffer.toString();
633         }
634 
635         /**
636          * Return the contents in the same format that is used when you call toString() on a List.
637          */
638         private String toListString() {
639             StringBuilder buffer = new StringBuilder();
640             buffer.append("[");
641             for (Item item : this) {
642                 if (buffer.length() > 1) {
643                     buffer.append(", ");
644                 }
645                 if (item instanceof ListItem) {
646                     buffer.append(((ListItem) item).toListString());
647                 } else {
648                     buffer.append(item);
649                 }
650             }
651             buffer.append("]");
652             return buffer.toString();
653         }
654     }
655 
656     public ComparableVersion(String version) {
657         parseVersion(version);
658     }
659 
660     @SuppressWarnings("checkstyle:innerassignment")
661     public final void parseVersion(String version) {
662         this.value = version;
663 
664         items = new ListItem();
665 
666         version = version.toLowerCase(Locale.ENGLISH);
667 
668         ListItem list = items;
669 
670         Deque<Item> stack = new ArrayDeque<>();
671         stack.push(list);
672 
673         boolean isDigit = false;
674 
675         boolean isCombination = false;
676 
677         int startIndex = 0;
678 
679         for (int i = 0; i < version.length(); i++) {
680             char c = version.charAt(i);
681 
682             if (c == '.') {
683                 if (i == startIndex) {
684                     list.add(IntItem.ZERO);
685                 } else {
686                     list.add(parseItem(isCombination, isDigit, version.substring(startIndex, i)));
687                 }
688                 isCombination = false;
689                 startIndex = i + 1;
690             } else if (c == '-') {
691                 if (i == startIndex) {
692                     list.add(IntItem.ZERO);
693                 } else {
694                     // X-1 is going to be treated as X1
695                     if (!isDigit && i != version.length() - 1) {
696                         char c1 = version.charAt(i + 1);
697                         if (Character.isDigit(c1)) {
698                             isCombination = true;
699                             continue;
700                         }
701                     }
702                     list.add(parseItem(isCombination, isDigit, version.substring(startIndex, i)));
703                 }
704                 startIndex = i + 1;
705 
706                 if (!list.isEmpty()) {
707                     list.add(list = new ListItem());
708                     stack.push(list);
709                 }
710                 isCombination = false;
711             } else if (Character.isDigit(c)) {
712                 if (!isDigit && i > startIndex) {
713                     // X1
714                     isCombination = true;
715 
716                     if (!list.isEmpty()) {
717                         list.add(list = new ListItem());
718                         stack.push(list);
719                     }
720                 }
721 
722                 isDigit = true;
723             } else {
724                 if (isDigit && i > startIndex) {
725                     list.add(parseItem(isCombination, true, version.substring(startIndex, i)));
726                     startIndex = i;
727 
728                     list.add(list = new ListItem());
729                     stack.push(list);
730                     isCombination = false;
731                 }
732 
733                 isDigit = false;
734             }
735         }
736 
737         if (version.length() > startIndex) {
738             // 1.0.0.X1 < 1.0.0-X2
739             // treat .X as -X for any string qualifier X
740             if (!isDigit && !list.isEmpty()) {
741                 list.add(list = new ListItem());
742                 stack.push(list);
743             }
744 
745             list.add(parseItem(isCombination, isDigit, version.substring(startIndex)));
746         }
747 
748         while (!stack.isEmpty()) {
749             list = (ListItem) stack.pop();
750             list.normalize();
751         }
752     }
753 
754     private static Item parseItem(boolean isDigit, String buf) {
755         return parseItem(false, isDigit, buf);
756     }
757 
758     private static Item parseItem(boolean isCombination, boolean isDigit, String buf) {
759         if (isCombination) {
760             return new CombinationItem(buf.replace("-", ""));
761         } else if (isDigit) {
762             buf = stripLeadingZeroes(buf);
763             if (buf.length() <= MAX_INTITEM_LENGTH) {
764                 // lower than 2^31
765                 return new IntItem(buf);
766             } else if (buf.length() <= MAX_LONGITEM_LENGTH) {
767                 // lower than 2^63
768                 return new LongItem(buf);
769             }
770             return new BigIntegerItem(buf);
771         }
772         return new StringItem(buf, false);
773     }
774 
775     private static String stripLeadingZeroes(String buf) {
776         if (buf == null || buf.isEmpty()) {
777             return "0";
778         }
779         for (int i = 0; i < buf.length(); ++i) {
780             char c = buf.charAt(i);
781             if (c != '0') {
782                 return buf.substring(i);
783             }
784         }
785         return buf;
786     }
787 
788     @Override
789     public int compareTo(ComparableVersion o) {
790         return items.compareTo(o.items);
791     }
792 
793     @Override
794     public String toString() {
795         return value;
796     }
797 
798     public String getCanonical() {
799         if (canonical == null) {
800             canonical = items.toString();
801         }
802         return canonical;
803     }
804 
805     @Override
806     public boolean equals(Object o) {
807         return (o instanceof ComparableVersion) && items.equals(((ComparableVersion) o).items);
808     }
809 
810     @Override
811     public int hashCode() {
812         return items.hashCode();
813     }
814 
815     // CHECKSTYLE_OFF: LineLength
816 
817     /**
818      * Main to test version parsing and comparison.
819      * <p>
820      * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue
821      * <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>
822      * command to command line. Result of given command will be something like this:
823      * <pre>
824      * Display parameters as parsed by Maven (in canonical form) and comparison result:
825      * 1. 1.2.7 == 1.2.7
826      *    1.2.7 &gt; 1.2-SNAPSHOT
827      * 2. 1.2-SNAPSHOT == 1.2-snapshot
828      * </pre>
829      *
830      * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always
831      *             two adjacent will be compared.
832      */
833     // CHECKSTYLE_ON: LineLength
834     public static void main(String... args) {
835         System.out.println("Display parameters as parsed by Maven (in canonical form and as a list of tokens) and"
836                 + " comparison result:");
837         if (args.length == 0) {
838             return;
839         }
840 
841         ComparableVersion prev = null;
842         int i = 1;
843         for (String version : args) {
844             ComparableVersion c = new ComparableVersion(version);
845 
846             if (prev != null) {
847                 int compare = prev.compareTo(c);
848                 System.out.println("   " + prev.toString() + ' ' + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">"))
849                         + ' ' + version);
850             }
851 
852             System.out.println(
853                     (i++) + ". " + version + " -> " + c.getCanonical() + "; tokens: " + c.items.toListString());
854 
855             prev = c;
856         }
857     }
858 }