View Javadoc

1   package com.atlassian.plugin.util;
2   
3   import org.apache.commons.lang.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  {
28      public static final String DELIMITER_PATTERN = "[\\.-]";
29      public static final String COMPONENT_PATTERN = "[\\d\\w]+";
30      public static final String VALID_VERSION_PATTERN = COMPONENT_PATTERN + "(?:" + DELIMITER_PATTERN + COMPONENT_PATTERN + ")*";
31      private static final Pattern START_WITH_INT_PATTERN = Pattern.compile("(^\\d+)");
32      public static final Pattern SNAPSHOT_PATTERN = Pattern.compile(".*-SNAPSHOT$");
33  
34      public static boolean isValidVersionString(final String version)
35      {
36          return (version != null) && version.matches(VALID_VERSION_PATTERN);
37      }
38  
39      public static boolean isSnapshotVersion(final String version)
40      {
41          return (version != null) && SNAPSHOT_PATTERN.matcher(version).matches();
42      }
43  
44      /**
45       * Compares two version strings. If either argument is not a String,
46       * this method returns 0.
47       *
48       * @throws IllegalArgumentException if either argument is a String,
49       * but does not match {@link #VALID_VERSION_PATTERN}.
50       * @see #isValidVersionString(String)
51       */
52      //    public int compare(Object o1, Object o2)
53      //    {
54      //        if (!(o1 instanceof String)) return 0;
55      //        if (!(o2 instanceof String)) return 0;
56      //
57      //        return compare((String) o1, (String) o2);
58      //    }
59      /**
60       * Compares two version strings using the algorithm described above.
61       *
62       * @return <tt>-1</tt> if version1 is before version2, <tt>1</tt> if version2 is before
63       * version1, or <tt>0</tt> if the versions are equal.
64       * @throws IllegalArgumentException if either argument does not match {@link #VALID_VERSION_PATTERN}.
65       * @see #isValidVersionString(String)
66       */
67      public int compare(final String version1, final String version2)
68      {
69          // Get the version numbers, remove all whitespaces
70          String thisVersion = "0";
71          if (StringUtils.isNotEmpty(version1))
72          {
73              thisVersion = version1.replaceAll(" ", "");
74          }
75          String compareVersion = "0";
76          if (StringUtils.isNotEmpty(version2))
77          {
78              compareVersion = version2.replaceAll(" ", "");
79          }
80  
81          if (!thisVersion.matches(VALID_VERSION_PATTERN) || !compareVersion.matches(VALID_VERSION_PATTERN))
82          {
83              throw new IllegalArgumentException("Version number '" + thisVersion + "' cannot be compared to '" + compareVersion + "'");
84          }
85  
86          // Split the version numbers
87          final String[] v1 = thisVersion.split(DELIMITER_PATTERN);
88          final String[] v2 = compareVersion.split(DELIMITER_PATTERN);
89  
90          final Comparator<String> componentComparator = new VersionStringComponentComparator();
91  
92          // Compare each place, until we find a difference and then return. If empty, assume zero.
93          for (int i = 0; i < (v1.length > v2.length ? v1.length : v2.length); i++)
94          {
95              final String component1 = i >= v1.length ? "0" : v1[i];
96              final String component2 = i >= v2.length ? "0" : v2[i];
97  
98              if (componentComparator.compare(component1, component2) != 0)
99              {
100                 return componentComparator.compare(component1, component2);
101             }
102         }
103 
104         return 0;
105     }
106 
107     private class VersionStringComponentComparator implements Comparator<String>
108     {
109         public static final int FIRST_GREATER = 1;
110         public static final int SECOND_GREATER = -1;
111 
112         public int compare(final String component1, final String component2)
113         {
114             if (component1.equalsIgnoreCase(component2))
115             {
116                 return 0;
117             }
118 
119             if (isInteger(component1) && isInteger(component2))
120             {
121                 return new BigInteger(component1).compareTo( new BigInteger(component2));
122             }
123 
124             // Handles the case where we are comparing 1.5 to 1.6a
125             final BigInteger comp1BigIntPart = getStartingInteger(component1);
126             final BigInteger comp2BigIntPart = getStartingInteger(component2);
127             if (comp1BigIntPart != null && comp2BigIntPart != null)
128             {
129                 if(comp1BigIntPart.compareTo(comp2BigIntPart) > 0){
130                     return FIRST_GREATER;
131                 }
132 
133                 if(comp2BigIntPart.compareTo(comp1BigIntPart) > 0){
134                     return SECOND_GREATER;
135                 }
136             }
137 
138             // 2.3-alpha < 2.3.0 and 2.3a < 2.3
139             // fixes PLUG-672. We are safe to do the integer check here since above we have
140             // already determined that one of the two components are not an integer and that one does not start with
141             // an int that may be larger than the other component
142             if (isInteger(component1))
143             {
144                 return FIRST_GREATER;
145             }
146             if (isInteger(component2))
147             {
148                 return SECOND_GREATER;
149             }
150 
151             // 2.3a < 2.3b
152             return component1.compareToIgnoreCase(component2);
153         }
154 
155         private boolean isInteger(final String string)
156         {
157             return string.matches("\\d+");
158         }
159 
160         private BigInteger getStartingInteger(final String string)
161         {
162             Matcher matcher = START_WITH_INT_PATTERN.matcher(string);
163             if (matcher.find())
164             {
165                 // If we found a starting digit group then lets return it
166                 return new BigInteger(matcher.group(1));
167             }
168             return null;
169         }
170 
171     }
172 }