View Javadoc

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