View Javadoc
1   package com.atlassian.plugin.util;
2   
3   import org.apache.commons.lang3.StringUtils;
4   
5   import java.math.BigInteger;
6   import java.util.Comparator;
7   import java.util.regex.Matcher;
8   import java.util.regex.Pattern;
9   
10  /**
11   * Compares dotted version strings of varying length. Makes a best effort with
12   * other delimiters and non-numeric versions.
13   * <p>
14   * For dotted decimals, comparison is as you'd expect: 0.1 is before 0.2 is before
15   * 1.0 is before 2.0. This works for any number of dots.
16   * <p>
17   * More complicated version numbers are compared by splitting the version strings
18   * into components using the {@link #DELIMITER_PATTERN} and comparing each
19   * component in order. The first difference found when comparing components
20   * left-to-right is returned.
21   * <p>
22   * Two numeric components (containing only digits) are compared as integers. A
23   * numeric component comes after any non-numeric one. Two non-numeric components
24   * are ordered by {@link String#compareToIgnoreCase(String)}.
25   */
26  public class VersionStringComparator implements Comparator<String> {
27      public static final String DELIMITER_PATTERN = "[\\.-]";
28      public static final String COMPONENT_PATTERN = "[\\d\\w]+";
29      public static final String VALID_VERSION_PATTERN = COMPONENT_PATTERN + "(?:" + DELIMITER_PATTERN + COMPONENT_PATTERN + ")*";
30      private static final Pattern START_WITH_INT_PATTERN = Pattern.compile("(^\\d+)");
31      public static final Pattern SNAPSHOT_PATTERN = Pattern.compile(".*-SNAPSHOT$");
32  
33      public static boolean isValidVersionString(final String version) {
34          return (version != null) && version.matches(VALID_VERSION_PATTERN);
35      }
36  
37      public static boolean isSnapshotVersion(final String version) {
38          return (version != null) && SNAPSHOT_PATTERN.matcher(version).matches();
39      }
40  
41      /**
42       * Compares two version strings. If either argument is not a String,
43       * this method returns 0.
44       *
45       * @throws IllegalArgumentException if either argument is a String,
46       * but does not match {@link #VALID_VERSION_PATTERN}.
47       * @see #isValidVersionString(String)
48       */
49      //    public int compare(Object o1, Object o2)
50      //    {
51      //        if (!(o1 instanceof String)) return 0;
52      //        if (!(o2 instanceof String)) return 0;
53      //
54      //        return compare((String) o1, (String) o2);
55      //    }
56  
57      /**
58       * Compares two version strings using the algorithm described above.
59       *
60       * @return <tt>-1</tt> if version1 is before version2, <tt>1</tt> if version2 is before
61       * version1, or <tt>0</tt> if the versions are equal.
62       * @throws IllegalArgumentException if either argument does not match {@link #VALID_VERSION_PATTERN}.
63       * @see #isValidVersionString(String)
64       */
65      public int compare(final String version1, final String version2) {
66          // Get the version numbers, remove all whitespaces
67          String thisVersion = "0";
68          if (StringUtils.isNotEmpty(version1)) {
69              thisVersion = version1.replaceAll(" ", "");
70          }
71          String compareVersion = "0";
72          if (StringUtils.isNotEmpty(version2)) {
73              compareVersion = version2.replaceAll(" ", "");
74          }
75  
76          if (!thisVersion.matches(VALID_VERSION_PATTERN) || !compareVersion.matches(VALID_VERSION_PATTERN)) {
77              throw new IllegalArgumentException("Version number '" + thisVersion + "' cannot be compared to '" + compareVersion + "'");
78          }
79  
80          // Split the version numbers
81          final String[] v1 = thisVersion.split(DELIMITER_PATTERN);
82          final String[] v2 = compareVersion.split(DELIMITER_PATTERN);
83  
84          final Comparator<String> componentComparator = new VersionStringComponentComparator();
85  
86          // Compare each place, until we find a difference and then return. If empty, assume zero.
87          for (int i = 0; i < (v1.length > v2.length ? v1.length : v2.length); i++) {
88              final String component1 = i >= v1.length ? "0" : v1[i];
89              final String component2 = i >= v2.length ? "0" : v2[i];
90  
91              if (componentComparator.compare(component1, component2) != 0) {
92                  return componentComparator.compare(component1, component2);
93              }
94          }
95  
96          return 0;
97      }
98  
99      private class VersionStringComponentComparator implements Comparator<String> {
100         public static final int FIRST_GREATER = 1;
101         public static final int SECOND_GREATER = -1;
102 
103         public int compare(final String component1, final String component2) {
104             if (component1.equalsIgnoreCase(component2)) {
105                 return 0;
106             }
107 
108             if (isInteger(component1) && isInteger(component2)) {
109                 return new BigInteger(component1).compareTo(new BigInteger(component2));
110             }
111 
112             // Handles the case where we are comparing 1.5 to 1.6a
113             final BigInteger comp1BigIntPart = getStartingInteger(component1);
114             final BigInteger comp2BigIntPart = getStartingInteger(component2);
115             if (comp1BigIntPart != null && comp2BigIntPart != null) {
116                 if (comp1BigIntPart.compareTo(comp2BigIntPart) > 0) {
117                     return FIRST_GREATER;
118                 }
119 
120                 if (comp2BigIntPart.compareTo(comp1BigIntPart) > 0) {
121                     return SECOND_GREATER;
122                 }
123             }
124 
125             // 2.3-alpha < 2.3.0 and 2.3a < 2.3
126             // fixes PLUG-672. We are safe to do the integer check here since above we have
127             // already determined that one of the two components are not an integer and that one does not start with
128             // an int that may be larger than the other component
129             if (isInteger(component1)) {
130                 return FIRST_GREATER;
131             }
132             if (isInteger(component2)) {
133                 return SECOND_GREATER;
134             }
135 
136             // 2.3a < 2.3b
137             return component1.compareToIgnoreCase(component2);
138         }
139 
140         private boolean isInteger(final String string) {
141             return string.matches("\\d+");
142         }
143 
144         private BigInteger getStartingInteger(final String string) {
145             Matcher matcher = START_WITH_INT_PATTERN.matcher(string);
146             if (matcher.find()) {
147                 // If we found a starting digit group then lets return it
148                 return new BigInteger(matcher.group(1));
149             }
150             return null;
151         }
152 
153     }
154 }